Compare commits

...

103 Commits

Author SHA1 Message Date
Mike Angell
2d0917493d Add performance rubocop and fixes 2019-08-27 22:07:44 +10:00
Mike Angell
ef8a6dcda1 Add option legacy file which will load removed constants 2019-08-27 17:46:09 +10:00
Mike Angell
0dd22af7d6 Use optimised 2.4 syntax 2019-08-27 17:46:09 +10:00
Mike Angell
da9051eb18 Upgrade rubocop 2019-08-27 17:46:09 +10:00
Justin Li
831355dfbd Merge pull request #1117 from ashmaroli/reduce-allocations-template-lookup-class
Reduce allocations while registering Liquid tags
2019-08-07 16:37:39 -04:00
Ashwin Maroli
00702d8e63 Use Object.const_get directly 2019-08-07 11:44:53 +05:30
Justin Li
197c058208 Merge pull request #1099 from ashmaroli/stash-types-private-constant
Use a private constant to stash token-types
2019-08-06 17:56:56 -04:00
Justin Li
98dfe198e1 Merge pull request #1115 from ashmaroli/reduce-allocations-from-truncate-filters
Reduce string allocations from truncate filters
2019-08-06 17:48:43 -04:00
Ashwin Maroli
c2c1497ca8 Reduce allocations while registering Liquid tags 2019-07-22 20:42:37 +05:30
Ashwin Maroli
d19967a79d Reduce string allocations from truncate filters 2019-07-22 17:35:45 +05:30
Florian Weingarten
248c54a386 Merge pull request #1091 from Shopify/rendering-with-less-garbage
Rendering with less garbage
2019-07-19 15:53:22 +01:00
Ashwin Maroli
2c42447659 Rename constant to SINGLE_TOKEN_EXPRESSION_TYPES 2019-05-17 23:30:24 +05:30
Ashwin Maroli
9ef6f9b642 Freeze mutable object assigned to constant 2019-04-29 23:50:49 +05:30
Ashwin Maroli
4684478e94 Use a private constant to stash token-types 2019-04-29 23:45:45 +05:30
Florian Weingarten
b3b63a683f Merge pull request #1097 from ashmaroli/stackprof-no-jruby
Don't attempt to install stackprof gem on JRuby
2019-04-24 09:11:44 -04:00
Ashwin Maroli
1c577c5b62 Don't attempt to install stackprof gem on JRuby 2019-04-24 11:31:20 +05:30
David Cornu
755d2821f3 Merge pull request #1094 from Shopify/for-tag/invalid-limit-offset
Make sure the for tag's limit and offset are integers
2019-04-23 17:20:54 -04:00
David Cornu
495b3d312f Merge pull request #1095 from Shopify/travis/remove-rainbow-gem
Stop installing the rainbow gem on Travis
2019-04-23 17:20:38 -04:00
Florian Weingarten
9640e77805 render_to_output_buffer 2019-04-23 17:06:29 -04:00
David Cornu
453f6348c2 Stop installing the rainbow gem on Travis 2019-04-23 16:55:37 -04:00
David Cornu
70ed1fc86d Make sure the limit and offset values are integers 2019-04-23 16:44:37 -04:00
Florian Weingarten
2a1ca3152d liquid without the garbage 2019-04-22 16:34:31 -04:00
Florian Weingarten
c2ef247be5 Merge pull request #1092 from Shopify/rake-memory-profiler-task
rake memory_profile:run
2019-04-22 16:33:32 -04:00
Florian Weingarten
1518d3f6f9 Merge pull request #1093 from Shopify/bytesize-not-length
use bytesize, not length
2019-04-18 18:39:21 +01:00
Florian Weingarten
c67b77709d rake memory_profile:run 2019-04-17 19:09:26 +01:00
Florian Weingarten
c89ce9c2ed use bytesize, not length 2019-04-17 18:55:13 +01:00
Richard Monette
b0629f17f7 Merge pull request #1073 from Shopify/defer-alloc-hash
defer hash allocation until needed for unparsed_args
2019-03-20 13:34:48 -04:00
Richard Monette
274f078806 defer hash allocation in parse_filter_expressions
add exploration of GC object allocation

remove performance test

can actually remove one more if branch

use named locals to improve readability
2019-03-20 13:20:31 -04:00
Richard Monette
d7171aa084 Merge pull request #1077 from Shopify/update-cops-for-trailing-comma
update Rubocop for trailing comma styles
2019-03-19 16:02:26 -04:00
Richard Monette
06c4789dc5 update Rubocop for trailing comma styles 2019-03-19 11:05:05 -04:00
Justin Li
f2f467bdbc v4.0.3 2019-03-12 12:43:48 -04:00
Justin Li
ff99d92c18 Merge pull request #1072 from Shopify/fix-interrupts
Fix interrupts through includes
2019-03-12 12:26:12 -04:00
Justin Li
39fecd06db Fix interrupts through includes 2019-03-12 12:18:22 -04:00
Justin Li
8013df8ca2 v4.0.2 2019-03-08 15:43:46 -05:00
Clayton Smith
14cd011cb5 Merge pull request #1070 from Shopify/url-decode-validation
Validate the character encoding in url_decode.
2019-03-08 11:09:40 -05:00
Clayton Smith
e2d9907df2 Validate the character encoding in url_decode. 2019-03-07 14:01:10 -05:00
Justin Li
23d669f5e6 Merge pull request #1032 from printercu/patch-1
Single regexp for strip_html
2019-02-22 13:04:04 -05:00
Justin Li
ed73794f82 Preserve existing strip_html behaviour for weird inputs 2019-02-22 13:00:36 -05:00
Ashwin Maroli
f59f6dea83 Fix simple RuboCop offenses and update TODO file (#1062)
* Fix Layout/EmptyLineAfterMagicComment offense

* Fix Layout/ExtraSpacing offense

* Fix Layout/ClosingParenthesisIndentation offenses

* Fix Style/MutableConstant offense

* Fix Style/UnneededInterpolation offenses

* Fix Style/RedundantParentheses offenses

* Update TODO config for RuboCop

* Add executable bit to test/test_helper.rb

ref: https://travis-ci.org/Shopify/liquid/jobs/488169512#L578
2019-02-22 12:32:56 -05:00
Garland Zhang
7a81fb821a Merge pull request #1059 from Shopify/map_error_checking
Apply error-checking to sort, sort_natural, where, uniq, map, compact filter(s)
2019-02-22 10:42:16 -05:00
Garland Zhang
cec27ea326 Extract raise error line and some filters with begin/rescue blocks 2019-02-21 17:00:20 -05:00
Justin Li
14999e8f7c Merge pull request #1053 from er1/update-changelog-v4.0.1
Updated changelog for v4.0.1 for (#1038)
2019-02-10 10:37:31 -05:00
Eric Chan
b41fc10d8e Updated changelog for v4.0.1 2018-12-03 23:54:00 -05:00
David Cornu
2b3c81cfd0 Merge pull request #1046 from Shopify/make-builds-green
Make builds green
2018-10-24 10:46:01 -04:00
David Cornu
2a2376bfd9 Run :test before :rubocop in the default Rake task 2018-10-19 15:06:36 -04:00
David Cornu
ca9e75db53 Reduce perceived complexity for #sort and #sort_natural 2018-10-19 14:57:33 -04:00
David Cornu
407c8abf30 Use TrailingCommaInLiteral
TrailingCommaInArrayLiteral and TrailingCommaInHashLiteral were introduced in v0.53.0 and we're running v0.49.0.

https://github.com/rubocop-hq/rubocop/blob/master/CHANGELOG.md#0530-2018-03-05
2018-10-19 14:52:16 -04:00
Justin Li
43f181e211 Merge pull request #1044 from Shopify/enable-cla-bot
Enable CLA bot
2018-10-19 09:34:30 -04:00
Tim Layton
7c613e87cb Enable CLA bot 2018-10-18 23:10:56 -07:00
Stephen Paul Weber (Work)
fe4034ccf9 Merge pull request #1025 from Shopify/traverse-ast
Liquid::ParseTreeVisitor
2018-10-18 09:42:56 -04:00
Stephen Paul Weber
52ee303a36 s/block.call/yield 2018-10-18 09:41:53 -04:00
Stephen Paul Weber
8217a8d86c Add test for the full array structure 2018-10-18 09:39:05 -04:00
Stephen Paul Weber
7d13d88258 s/Traversal/ParseTreeVisitor 2018-10-18 09:38:33 -04:00
Stephen Paul Weber
ff727016ef s/callback_for/add_callback_for 2018-10-18 09:37:48 -04:00
Stephen Paul Weber
c11fc656cf Colocate Traversal classes with classes they traverse
This puts all knowledge of the traversal in the same file, and removes
the need for a CASES registry.
2018-10-18 09:37:48 -04:00
Stephen Paul Weber
d789ec4175 Liquid::Traversal
This enables traversal over whole document tree.
2018-10-15 10:11:58 -04:00
Samuel Doiron
fd09f049b0 Merge pull request #1026 from Shopify/where-filter
Add `where` filter to standard filters
2018-10-11 17:45:30 -04:00
Samuel
842986a972 Add where filter to standard filters
Users of Liquid will often wish to filter an array to only those items that match a certain criteria. For example, showing "pinned" messages at the top of a list.

Example usage:

`{{ comments | where: "pinned" | first }}`

or

`{{ products | where: "category", "kitchen" }}`

* Add where filter to standard filters
* Add tests for new where functionality
2018-10-11 16:52:32 -04:00
Florian Weingarten
4661700a97 bump to v4.0.1 2018-10-09 11:13:19 +02:00
Justin Li
cd5a6dd225 Merge pull request #930 from er1/fix-sort-natural-on-nil
Fix sort and sort_natural on sorting with non-string and nil values
2018-10-04 22:32:37 -04:00
printercu
89c1ba2b0e Fix rubocop warning 2018-09-27 17:24:01 +03:00
printercu
479d8fb4a4 Single regexp for strip_html 2018-09-27 17:13:35 +03:00
Justin Li
53b8babf52 Merge pull request #1027 from Shopify/rubocop-fix
Update deprecated rubocop name
2018-09-13 17:16:36 -04:00
Justin Li
76b4920d3e Update deprecated rubocop name 2018-09-13 17:15:32 -04:00
Justin Li
8dcc319128 Merge pull request #1024 from koic/suppress_warning_bigdecimal_new
Suppress warning: `BigDecimal.new` is deprecated
2018-09-09 08:28:24 -04:00
Koichi ITO
0b36461d80 Suppress warning: BigDecimal.new is deprecated
## Summary

`BigDecimal.new` is deprecated since BigDecimal 1.3.3 for Ruby 2.5.

This PR suppresses the following warnings.

```console
% ruby -v
ruby 2.6.0dev (2018-09-06 trunk 64648) [x86_64-darwin17]
% RUBYOPT=-w bundle exec rake
(snip)
/Users/koic/src/github.com/Shopify/liquid/lib/liquid/utils.rb:49:
warning: BigDecimal.new is deprecated; use Kernel.BigDecimal method
instead.
/Users/koic/src/github.com/Shopify/liquid/lib/liquid/utils.rb:53:
warning: BigDecimal.new is deprecated; use Kernel.BigDecimal method
instead.
```

## Other Information

The following is a change of BigDecimal 1.3.3 for Ruby 2.5 related to this PR.

- 533737338d
- 16738ad0ac
2018-09-09 21:10:20 +09:00
Justin Li
70e75719de Merge pull request #1010 from Shopify/circle-ci-remove-38409b
Goodbye CircleCI 👋
2018-05-15 10:53:51 -04:00
shopify-admins
b037b19688 Removing CircleCI 1.0 [ci skip] 2018-05-15 10:35:38 -04:00
Florian Weingarten
d0f77f6cf4 Merge pull request #1006 from Benhgift/master
add installation instruction
2018-04-26 18:20:02 +01:00
Ben Gift
0be260bc97 add installation instruction 2018-04-26 08:12:47 -07:00
Dylan Thacker-Smith
5f0b64cebc Merge pull request #1005 from christopheraue/render_refactor
Refactored and optimized rendering
2018-04-19 16:44:57 -04:00
Christopher Aue
c086017bc9 refactored and optimized rendering
Measures:
1) A while loop is faster than iterating with #each.
2) Check string, variable and block tokens first. They are far more
   frequent than interrupt tokens. In their case, checking for an
   interrupt can be avoided.
3) String tokens just map to themselves and don't need the special
   treatment of BlockBody#render_node (except the resource limit
   check).

Benchmark
=========

$ bundle exec rake benchmark:run

Before
------

Run 1)
              parse:     41.630  (± 0.0%) i/s -    420.000  in  10.089309s
             render:     75.962  (± 3.9%) i/s -    763.000  in  10.066823s
     parse & render:     25.497  (± 0.0%) i/s -    256.000  in  10.040862s

Run 2)
              parse:     42.130  (± 0.0%) i/s -    424.000  in  10.064738s
             render:     77.003  (± 1.3%) i/s -    777.000  in  10.093524s
     parse & render:     25.739  (± 0.0%) i/s -    258.000  in  10.024581s

Run 3)
              parse:     41.976  (± 2.4%) i/s -    420.000  in  10.021406s
             render:     76.184  (± 1.3%) i/s -    763.000  in  10.018104s
     parse & render:     25.641  (± 0.0%) i/s -    258.000  in  10.062549s

After
-----

Run 1)
              parse:     42.283  (± 0.0%) i/s -    424.000  in  10.028306s
             render:     83.158  (± 2.4%) i/s -    832.000  in  10.009201s
     parse & render:     26.417  (± 0.0%) i/s -    266.000  in  10.069718s

Run 2)
              parse:     41.159  (± 4.9%) i/s -    412.000  in  10.031297s
             render:     81.591  (± 3.7%) i/s -    816.000  in  10.018225s
     parse & render:     25.924  (± 3.9%) i/s -    260.000  in  10.035653s

Run 3)
              parse:     42.418  (± 2.4%) i/s -    424.000  in  10.003100s
             render:     84.183  (± 2.4%) i/s -    847.000  in  10.069781s
     parse & render:     26.726  (± 0.0%) i/s -    268.000  in  10.029857s
2018-04-19 12:10:15 +02:00
Dylan Thacker-Smith
4369fe6c85 Improve the unexpected end delimiter message for block tags. (#1003) 2018-04-05 11:18:13 -04:00
Justin Li
c118e6b435 Merge pull request #992 from ashmaroli/each-without-index
Replace unnecessary Array#each_with_index with Array#each
2018-03-16 14:28:05 -04:00
Ashwin Maroli
0fbaf873d9 replace unnecessary #each_with_index with #each 2018-03-16 14:31:43 +05:30
Justin Li
5980ddbfae Merge pull request #988 from ashmaroli/regex-to-constant
Assign regexps to constants
2018-03-14 16:49:17 -04:00
Ashwin Maroli
193fc0fb7a revert to earlier regex for matching floats 2018-03-14 07:02:04 +05:30
Ashwin Maroli
e4da4d49d2 assign regex to a constant 2018-03-13 23:36:56 +05:30
Justin Li
a0bec1f873 Merge pull request #981 from nicolasleger/patch-1
[CI] Test against Ruby 2.5 version
2018-03-05 11:23:18 -05:00
Nicolas Leger
4aa3261518 [CI] Test against Ruby 2.5 version 2018-02-12 00:23:06 +01:00
Dylan Thacker-Smith
04d552fabb Gemfile: Use https rather than git protocol to fetch liquid-c 2018-02-01 07:08:19 -05:00
Dylan Thacker-Smith
5106466a2d Add a regression test for a liquid-c trim mode bug (#972) 2018-01-25 10:55:01 -05:00
Justin Li
5d6c1ed7c6 Merge pull request #963 from lostapathy/patch-1
have travis test against ruby 2.4
2017-12-15 16:53:26 -05:00
Joe Francis
a594653a0c have travis test against ruby 2.4 2017-12-15 14:27:17 -06:00
Thibaut Courouble
0c802aba17 Merge pull request #958 from Shopify/minmax
Rename min/max filters for clarity
2017-12-06 11:41:12 -05:00
Thibaut Courouble
147d7ae24d Rename min/max filters for clarity 2017-12-06 09:48:30 -05:00
Thibaut Courouble
282d42f98d Fix min/max filters 2017-12-06 08:58:05 -05:00
Justin Li
e6ba6ee87b Revert "Use replacement string for replace filters literally (#924)"
This reverts commit 27c91203ab.
2017-12-04 15:07:59 -05:00
Nithin Bekal
2ad7a37d44 Merge pull request #954 from Shopify/max-min-filters
Add max and min filters
2017-11-30 14:18:43 -05:00
Nithin Bekal
4bdaaf069f Add max/min filters 2017-11-30 13:56:37 -05:00
Justin Li
85b1e91aed Merge pull request #952 from Shopify/bump-rubocop
Bump rubocop
2017-11-22 12:44:36 -05:00
Justin Li
a7c5e247c8 Bump rubocop 2017-11-22 11:59:06 -05:00
Dylan Thacker-Smith
6c117fd7dd refactor: Reduce maximum block nesting in Liquid::BlockBody#parse (#944) 2017-10-19 10:12:40 -04:00
Maxime Bedard
7d2d90d715 Merge pull request #932 from Shopify/avoid-default-values-hash
Avoid hash with default values due to inconsistent marshaling
2017-10-17 16:02:45 -04:00
Maxime Bedard
f761d21215 Use {} notation 2017-09-20 09:48:23 -04:00
Maxime Bedard
a796c17f8b Avoid hash with default values due to inconsistent marshalling 2017-09-19 16:23:14 -04:00
Eric Chan
deb10ebc7a Sorting support for data with undefined values 2017-09-14 02:00:43 -04:00
Eric Chan
cfe1844de9 Added test coverage for sort_natural 2017-09-13 22:17:59 -04:00
Eric Chan
59950bff87 Fix sort_natural on sorting with non-string values 2017-09-13 01:37:40 -04:00
Dylan Thacker-Smith
27c91203ab Use replacement string for replace filters literally (#924) 2017-08-28 11:51:20 -04:00
Justin Li
44eaa4b9d8 Merge pull request #920 from Shopify/symbol_to_liquid
Support rendering symbols as strings
2017-08-18 12:10:53 -04:00
Pascal Betz
a979b3ec95 Do not raise when variable is defined but nil when using strict_variables 2017-08-18 12:09:57 -04:00
Justin Li
bf3e759da3 Support rendering symbols as strings 2017-08-17 23:10:57 -04:00
127 changed files with 3882 additions and 1694 deletions

2
.github/probots.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
enabled:
- cla

2
.gitignore vendored
View File

@@ -4,6 +4,8 @@
pkg pkg
*.rbc *.rbc
.rvmrc .rvmrc
.idea
.ruby-version .ruby-version
Gemfile.lock Gemfile.lock
.bundle .bundle
.byebug_history

1027
.rubocop.shopify.yml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,125 +1,10 @@
inherit_from: ./.rubocop_todo.yml inherit_from:
- .rubocop.shopify.yml
AllCops: require: rubocop-performance
Exclude:
- 'performance/shopify/*'
- 'pkg/**'
Metrics/BlockNesting: Performance:
Max: 3 Enabled: true
Exclude:
- 'lib/liquid/block_body.rb'
Metrics/ModuleLength: Metrics/LineLength:
Enabled: false
Metrics/ClassLength:
Enabled: false
Lint/AssignmentInCondition:
Enabled: false
Lint/AmbiguousOperator:
Enabled: false
Lint/AmbiguousRegexpLiteral:
Enabled: false
Lint/ParenthesesAsGroupedExpression:
Enabled: false
Lint/UnusedBlockArgument:
Enabled: false
Lint/EndAlignment:
AlignWith: variable
Lint/UnusedMethodArgument:
Enabled: false
Style/SingleLineBlockParams:
Enabled: false
Style/DoubleNegation:
Enabled: false
Style/StringLiteralsInInterpolation:
Enabled: false
Style/AndOr:
Enabled: false
Style/SignalException:
Enabled: false
Style/StringLiterals:
Enabled: false
Style/BracesAroundHashParameters:
Enabled: false
Style/NumericLiterals:
Enabled: false
Style/SpaceInsideBrackets:
Enabled: false
Style/SpaceBeforeBlockBraces:
Enabled: false
Style/Documentation:
Enabled: false
Style/ClassAndModuleChildren:
Enabled: false
Style/TrailingComma:
Enabled: false
Style/IndentHash:
EnforcedStyle: consistent
Style/FormatString:
Enabled: false
Style/AlignParameters:
EnforcedStyle: with_fixed_indentation
Style/MultilineOperationIndentation:
EnforcedStyle: indented
Style/IfUnlessModifier:
Enabled: false
Style/RaiseArgs:
Enabled: false
Style/DeprecatedHashMethods:
Enabled: false
Style/RegexpLiteral:
Enabled: false
Style/SymbolLiteral:
Enabled: false
Performance/Count:
Enabled: false
Style/ConstantName:
Enabled: false
Style/CaseIndentation:
Enabled: false
Style/ClassVars:
Enabled: false
Style/PerlBackrefs:
Enabled: false
Style/TrivialAccessors:
AllowPredicates: true
Style/WordArray:
Enabled: false Enabled: false

View File

@@ -1,72 +0,0 @@
# This configuration was generated by `rubocop --auto-gen-config`
# on 2015-06-08 18:16:16 +0000 using RuboCop version 0.32.0.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again.
# Offense count: 5
Lint/NestedMethodDefinition:
Enabled: false
# Offense count: 53
Metrics/AbcSize:
Max: 58
# Offense count: 12
Metrics/CyclomaticComplexity:
Max: 15
# Offense count: 553
# Configuration parameters: AllowURI, URISchemes.
Metrics/LineLength:
Max: 294
# Offense count: 77
# Configuration parameters: CountComments.
Metrics/MethodLength:
Max: 46
# Offense count: 6
Metrics/PerceivedComplexity:
Max: 13
# Offense count: 1
Style/AccessorMethodName:
Enabled: false
# Offense count: 1
# Cop supports --auto-correct.
Style/ClosingParenthesisIndentation:
Enabled: false
# Offense count: 3
# Configuration parameters: MinBodyLength.
Style/GuardClause:
Enabled: false
# Offense count: 4
# Configuration parameters: EnforcedStyle, SupportedStyles.
Style/MethodName:
Enabled: false
# Offense count: 1
Style/MultilineBlockChain:
Enabled: false
# Offense count: 2
# Configuration parameters: EnforcedStyle, MinBodyLength, SupportedStyles.
Style/Next:
Enabled: false
# Offense count: 7
# Cop supports --auto-correct.
# Configuration parameters: AllowAsExpressionSeparator.
Style/Semicolon:
Enabled: false
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: MaxLineLength.
Style/WhileUntilModifier:
Enabled: false

View File

@@ -1,12 +1,13 @@
language: ruby language: ruby
rvm: rvm:
- 2.1 - 2.4
- 2.2 - 2.5
- 2.3.3 - 2.6
- 2.7
- ruby-head - ruby-head
- jruby-head - jruby-head
# - rbx-2 - truffleruby
sudo: false sudo: false
@@ -17,13 +18,13 @@ addons:
matrix: matrix:
allow_failures: allow_failures:
- rvm: ruby-head
- rvm: jruby-head - rvm: jruby-head
install: install:
- gem install rainbow -v 2.2.1
- bundle install - bundle install
script: "bundle exec rake" script: bundle exec rake
notifications: notifications:
disable: true disable: true

19
Gemfile
View File

@@ -1,17 +1,26 @@
# frozen_string_literal: true
source 'https://rubygems.org' source 'https://rubygems.org'
git_source(:github) do |repo_name|
"https://github.com/#{repo_name}.git"
end
gemspec gemspec
gem 'stackprof', platforms: :mri
group :benchmark, :test do group :benchmark, :test do
gem 'benchmark-ips' gem 'benchmark-ips'
gem 'memory_profiler'
install_if -> { RUBY_PLATFORM !~ /mingw|mswin|java/ && RUBY_ENGINE != 'truffleruby' } do
gem 'stackprof'
end
end end
group :test do group :test do
gem 'rubocop', '0.34.2' gem 'rubocop', '~> 0.74.0', require: false
gem 'rubocop-performance', require: false
platform :mri do platform :mri, :truffleruby do
gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'bd53db95de3d44d631e7c5a267c3d934e66107dd' gem 'liquid-c', github: 'Shopify/liquid-c', ref: '7ba926791ef8411984d0f3e41c6353fd716041c6'
end end
end end

View File

@@ -1,5 +1,58 @@
# Liquid Change Log # Liquid Change Log
## 4.0.3 / 2019-03-12
### Fixed
* Fix break and continue tags inside included templates in loops (#1072) [Justin Li]
## 4.0.2 / 2019-03-08
### Changed
* Add `where` filter (#1026) [Samuel Doiron]
* Add `ParseTreeVisitor` to iterate the Liquid AST (#1025) [Stephen Paul Weber]
* Improve `strip_html` performance (#1032) [printercu]
### Fixed
* Add error checking for invalid combinations of inputs to sort, sort_natural, where, uniq, map, compact filters (#1059) [Garland Zhang]
* Validate the character encoding in url_decode (#1070) [Clayton Smith]
## 4.0.1 / 2018-10-09
### Changed
* Add benchmark group in Gemfile (#855) [Jerry Liu]
* Allow benchmarks to benchmark render by itself (#851) [Jerry Liu]
* Avoid calling `line_number` on String node when rescuing a render error. (#860) [Dylan Thacker-Smith]
* Avoid duck typing to detect whether to call render on a node. [Dylan Thacker-Smith]
* Clarify spelling of `reversed` on `for` block tag (#843) [Mark Crossfield]
* Replace recursion with loop to avoid potential stack overflow from malicious input (#891, #892) [Dylan Thacker-Smith]
* Limit block tag nesting to 100 (#894) [Dylan Thacker-Smith]
* Replace `assert_equal nil` with `assert_nil` (#895) [Dylan Thacker-Smith]
* Remove Spy Gem (#896) [Dylan Thacker-Smith]
* Add `collection_name` and `variable_name` reader to `For` block (#909)
* Symbols render as strings (#920) [Justin Li]
* Remove default value from Hash objects (#932) [Maxime Bedard]
* Remove one level of nesting (#944) [Dylan Thacker-Smith]
* Update Rubocop version (#952) [Justin Li]
* Add `at_least` and `at_most` filters (#954, #958) [Nithin Bekal]
* Add a regression test for a liquid-c trim mode bug (#972) [Dylan Thacker-Smith]
* Use https rather than git protocol to fetch liquid-c [Dylan Thacker-Smith]
* Add tests against Ruby 2.4 (#963) and 2.5 (#981)
* Replace RegExp literals with constants (#988) [Ashwin Maroli]
* Replace unnecessary `#each_with_index` with `#each` (#992) [Ashwin Maroli]
* Improve the unexpected end delimiter message for block tags. (#1003) [Dylan Thacker-Smith]
* Refactor and optimize rendering (#1005) [Christopher Aue]
* Add installation instruction (#1006) [Ben Gift]
* Remove Circle CI (#1010)
* Rename deprecated `BigDecimal.new` to `BigDecimal` (#1024) [Koichi ITO]
* Rename deprecated Rubocop name (#1027) [Justin Li]
### Fixed
* Handle `join` filter on non String joiners (#857) [Richard Monette]
* Fix duplicate inclusion condition logic error of `Liquid::Strainer.add_filter` method (#861)
* Fix `escape`, `url_encode`, `url_decode` not handling non-string values (#898) [Thierry Joyal]
* Fix raise when variable is defined but nil when using `strict_variables` [Pascal Betz]
* Fix `sort` and `sort_natural` to handle arrays with nils (#930) [Eric Chan]
## 4.0.0 / 2016-12-14 / branch "4-0-stable" ## 4.0.0 / 2016-12-14 / branch "4-0-stable"
### Changed ### Changed
@@ -221,7 +274,7 @@ Yanked from rubygems, as it contained too many changes that broke compatibility.
* Fixed bad error reporting when a filter called a method which doesn't exist. Liquid told you that it couldn't find the filter which was obviously misleading [Tobias Luetke] * Fixed bad error reporting when a filter called a method which doesn't exist. Liquid told you that it couldn't find the filter which was obviously misleading [Tobias Luetke]
* Removed count helper from standard lib. use size [Tobias Luetke] * Removed count helper from standard lib. use size [Tobias Luetke]
* Fixed bug with string filter parameters failing to tolerate commas in strings. [Paul Hammond] * Fixed bug with string filter parameters failing to tolerate commas in strings. [Paul Hammond]
* Improved filter parameters. Filter parameters are now context sensitive; Types are resolved according to the rules of the context. Multiple parameters are now separated by the Liquid::ArgumentSeparator: , by default [Paul Hammond] * Improved filter parameters. Filter parameters are now context sensitive; Types are resolved according to the rules of the context. Multiple parameters are now separated by the Liquid::ARGUMENT_SEPARATOR: , by default [Paul Hammond]
{{ 'Typo' | link_to: 'http://typo.leetsoft.com', 'Typo - a modern weblog engine' }} {{ 'Typo' | link_to: 'http://typo.leetsoft.com', 'Typo - a modern weblog engine' }}
* Added Liquid::Drop. A base class which you can use for exporting proxy objects to liquid which can acquire more data when used in liquid. [Tobias Luetke] * Added Liquid::Drop. A base class which you can use for exporting proxy objects to liquid which can acquire more data when used in liquid. [Tobias Luetke]

View File

@@ -42,6 +42,8 @@ Liquid is a template engine which was written with very specific requirements:
## How to use Liquid ## How to use Liquid
Install Liquid by adding `gem 'liquid'` to your gemfile.
Liquid supports a very simple API based around the Liquid::Template class. Liquid supports a very simple API based around the Liquid::Template class.
For standard use you can just pass it the content of a file and call render with a parameters hash. For standard use you can just pass it the content of a file and call render with a parameters hash.

View File

@@ -1,29 +1,33 @@
# frozen_string_literal: true
require 'rake' require 'rake'
require 'rake/testtask' require 'rake/testtask'
$LOAD_PATH.unshift File.expand_path("../lib", __FILE__) $LOAD_PATH.unshift(File.expand_path('lib', __dir__))
require "liquid/version" require 'liquid/version'
task default: [:rubocop, :test] task(default: %i[test rubocop])
desc 'run test suite with default parser' desc('run test suite with default parser')
Rake::TestTask.new(:base_test) do |t| Rake::TestTask.new(:base_test) do |t|
t.libs << '.' << 'lib' << 'test' t.libs << '.' << 'lib' << 'test'
t.test_files = FileList['test/{integration,unit}/**/*_test.rb'] t.test_files = FileList['test/{integration,unit}/**/*_test.rb']
t.verbose = false t.verbose = false
end end
desc 'run test suite with warn error mode' desc('run test suite with warn error mode')
task :warn_test do task :warn_test do
ENV['LIQUID_PARSER_MODE'] = 'warn' ENV['LIQUID_PARSER_MODE'] = 'warn'
Rake::Task['base_test'].invoke Rake::Task['base_test'].invoke
end end
task :rubocop do task :rubocop do
require 'rubocop/rake_task' if RUBY_ENGINE == 'ruby'
RuboCop::RakeTask.new require 'rubocop/rake_task'
RuboCop::RakeTask.new
end
end end
desc 'runs test suite with both strict and lax parsers' desc('runs test suite with both strict and lax parsers')
task :test do task :test do
ENV['LIQUID_PARSER_MODE'] = 'lax' ENV['LIQUID_PARSER_MODE'] = 'lax'
Rake::Task['base_test'].invoke Rake::Task['base_test'].invoke
@@ -32,8 +36,9 @@ task :test do
Rake::Task['base_test'].reenable Rake::Task['base_test'].reenable
Rake::Task['base_test'].invoke Rake::Task['base_test'].invoke
if RUBY_ENGINE == 'ruby' if RUBY_ENGINE == 'ruby' || RUBY_ENGINE == 'truffleruby'
ENV['LIQUID-C'] = '1'
ENV['LIQUID_C'] = '1'
ENV['LIQUID_PARSER_MODE'] = 'lax' ENV['LIQUID_PARSER_MODE'] = 'lax'
Rake::Task['base_test'].reenable Rake::Task['base_test'].reenable
@@ -45,9 +50,9 @@ task :test do
end end
end end
task gem: :build task(gem: :build)
task :build do task :build do
system "gem build liquid.gemspec" system 'gem build liquid.gemspec'
end end
task install: :build do task install: :build do
@@ -56,38 +61,45 @@ end
task release: :build do task release: :build do
system "git tag -a v#{Liquid::VERSION} -m 'Tagging #{Liquid::VERSION}'" system "git tag -a v#{Liquid::VERSION} -m 'Tagging #{Liquid::VERSION}'"
system "git push --tags" system 'git push --tags'
system "gem push liquid-#{Liquid::VERSION}.gem" system "gem push liquid-#{Liquid::VERSION}.gem"
system "rm liquid-#{Liquid::VERSION}.gem" system "rm liquid-#{Liquid::VERSION}.gem"
end end
namespace :benchmark do namespace :benchmark do
desc "Run the liquid benchmark with lax parsing" desc 'Run the liquid benchmark with lax parsing'
task :run do task :run do
ruby "./performance/benchmark.rb lax" ruby './performance/benchmark.rb lax'
end end
desc "Run the liquid benchmark with strict parsing" desc 'Run the liquid benchmark with strict parsing'
task :strict do task :strict do
ruby "./performance/benchmark.rb strict" ruby './performance/benchmark.rb strict'
end end
end end
namespace :profile do namespace :profile do
desc "Run the liquid profile/performance coverage" desc 'Run the liquid profile/performance coverage'
task :run do task :run do
ruby "./performance/profile.rb" ruby './performance/profile.rb'
end end
desc "Run the liquid profile/performance coverage with strict parsing" desc 'Run the liquid profile/performance coverage with strict parsing'
task :strict do task :strict do
ruby "./performance/profile.rb strict" ruby './performance/profile.rb strict'
end end
end end
desc "Run example" namespace :memory_profile do
desc 'Run memory profiler'
task :run do
ruby './performance/memory_profile.rb'
end
end
desc('Run example')
task :example do task :example do
ruby "-w -d -Ilib example/server/server.rb" ruby '-w -d -Ilib example/server/server.rb'
end end
task :console do task :console do

View File

@@ -1,3 +0,0 @@
machine:
ruby:
version: ruby-2.1

View File

@@ -1,6 +1,8 @@
# frozen_string_literal: true
module ProductsFilter module ProductsFilter
def price(integer) def price(integer)
sprintf("$%.2d USD", integer / 100.0) format('$%.2d USD', integer / 100.0)
end end
def prettyprint(text) def prettyprint(text)
@@ -28,17 +30,17 @@ class Servlet < LiquidServlet
private private
def products_list def products_list
[{ 'name' => 'Arbor Draft', 'price' => 39900, 'description' => 'the *arbor draft* is a excellent product' }, [{ 'name' => 'Arbor Draft', 'price' => 39_900, 'description' => 'the *arbor draft* is a excellent product' },
{ 'name' => 'Arbor Element', 'price' => 40000, 'description' => 'the *arbor element* rocks for freestyling' }, { 'name' => 'Arbor Element', 'price' => 40_000, 'description' => 'the *arbor element* rocks for freestyling' },
{ 'name' => 'Arbor Diamond', 'price' => 59900, 'description' => 'the *arbor diamond* is a made up product because im obsessed with arbor and have no creativity' }] { 'name' => 'Arbor Diamond', 'price' => 59_900, 'description' => 'the *arbor diamond* is a made up product because im obsessed with arbor and have no creativity' }]
end end
def more_products_list def more_products_list
[{ 'name' => 'Arbor Catalyst', 'price' => 39900, 'description' => 'the *arbor catalyst* is an advanced drop-through for freestyle and flatground performance and versatility' }, [{ 'name' => 'Arbor Catalyst', 'price' => 39_900, 'description' => 'the *arbor catalyst* is an advanced drop-through for freestyle and flatground performance and versatility' },
{ 'name' => 'Arbor Fish', 'price' => 40000, 'description' => 'the *arbor fish* is a compact pin that features an extended wheelbase and time-honored teardrop shape' }] { 'name' => 'Arbor Fish', 'price' => 40_000, 'description' => 'the *arbor fish* is a compact pin that features an extended wheelbase and time-honored teardrop shape' }]
end end
def description def description
"List of Products ~ This is a list of products with price and description." 'List of Products ~ This is a list of products with price and description.'
end end
end end

View File

@@ -1,23 +1,28 @@
# frozen_string_literal: true
class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet
def do_GET(req, res) def get(req, res)
handle(:get, req, res) handle(:get, req, res)
end end
def do_POST(req, res) def post(req, res)
handle(:post, req, res) handle(:post, req, res)
end end
alias_method :do_GET, :get
alias_method :do_POST, :post
private private
def handle(type, req, res) def handle(_type, req, res)
@request = req @request = req
@response = res @response = res
@request.path_info =~ /(\w+)\z/ @request.path_info =~ /(\w+)\z/
@action = $1 || 'index' @action = Regexp.last_match(1) || 'index'
@assigns = send(@action) if respond_to?(@action) @assigns = send(@action) if respond_to?(@action)
@response['Content-Type'] = "text/html" @response['Content-Type'] = 'text/html'
@response.status = 200 @response.status = 200
@response.body = Liquid::Template.parse(read_template).render(@assigns, filters: [ProductsFilter]) @response.body = Liquid::Template.parse(read_template).render(@assigns, filters: [ProductsFilter])
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'webrick' require 'webrick'
require 'rexml/document' require 'rexml/document'
@@ -8,5 +10,5 @@ require_relative 'example_servlet'
# Setup webrick # Setup webrick
server = WEBrick::HTTPServer.new(Port: ARGV[1] || 3000) server = WEBrick::HTTPServer.new(Port: ARGV[1] || 3000)
server.mount('/', Servlet) server.mount('/', Servlet)
trap("INT"){ server.shutdown } trap('INT') { server.shutdown }
server.start server.start

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (c) 2005 Tobias Luetke # Copyright (c) 2005 Tobias Luetke
# #
# Permission is hereby granted, free of charge, to any person obtaining # Permission is hereby granted, free of charge, to any person obtaining
@@ -20,31 +22,32 @@
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
module Liquid module Liquid
FilterSeparator = /\|/ FILTER_SEPARATOR = /\|/.freeze
ArgumentSeparator = ','.freeze ARGUMENT_SEPARATOR = ','
FilterArgumentSeparator = ':'.freeze FILTER_ARGUMENT_SEPARATOR = ':'
VariableAttributeSeparator = '.'.freeze VARIABLE_ATTRIBUTE_SEPARATOR = '.'
WhitespaceControl = '-'.freeze WHITESPACE_CONTROL = '-'
TagStart = /\{\%/ TAG_START = /\{\%/.freeze
TagEnd = /\%\}/ TAG_END = /\%\}/.freeze
VariableSignature = /\(?[\w\-\.\[\]]\)?/ VARIABLE_SIGNATURE = /\(?[\w\-\.\[\]]\)?/.freeze
VariableSegment = /[\w\-]/ VARIABLE_SEGMENT = /[\w\-]/.freeze
VariableStart = /\{\{/ VARIABLE_START = /\{\{/.freeze
VariableEnd = /\}\}/ VARIABLE_END = /\}\}/.freeze
VariableIncompleteEnd = /\}\}?/ VARIABLE_INCOMPLETE_END = /\}\}?/.freeze
QuotedString = /"[^"]*"|'[^']*'/ QUOTED_STRING = /"[^"]*"|'[^']*'/.freeze
QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o QUOTED_FRAGMENT = /#{QUOTED_STRING}|(?:[^\s,\|'"]|#{QUOTED_STRING})+/o.freeze
TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/o TAG_ATTRIBUTES = /(\w+)\s*\:\s*(#{QUOTED_FRAGMENT})/o.freeze
AnyStartingTag = /#{TagStart}|#{VariableStart}/o ANY_STARTING_TAG = /#{TAG_START}|#{VARIABLE_START}/o.freeze
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om PARTIAL_TEMPLATE_PARSER = /#{TAG_START}.*?#{TAG_END}|#{VARIABLE_START}.*?#{VARIABLE_INCOMPLETE_END}/om.freeze
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om TEMPLATE_PARSER = /(#{PARTIAL_TEMPLATE_PARSER}|#{ANY_STARTING_TAG})/om.freeze
VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o VARIABLE_PARSER = /\[[^\]]+\]|#{VARIABLE_SEGMENT}+\??/o.freeze
singleton_class.send(:attr_accessor, :cache_classes) singleton_class.send(:attr_accessor, :cache_classes)
self.cache_classes = true self.cache_classes = true
end end
require "liquid/version" require 'liquid/version'
require 'liquid/parse_tree_visitor'
require 'liquid/lexer' require 'liquid/lexer'
require 'liquid/parser' require 'liquid/parser'
require 'liquid/i18n' require 'liquid/i18n'

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
class Block < Tag class Block < Tag
MAX_DEPTH = 100 MAX_DEPTH = 100
@@ -13,6 +15,7 @@ module Liquid
end end
end end
# For backwards compatibility
def render(context) def render(context)
@body.render(context) @body.render(context)
end end
@@ -26,16 +29,16 @@ module Liquid
end end
def unknown_tag(tag, _params, _tokens) def unknown_tag(tag, _params, _tokens)
case tag if tag == 'else'
when 'else'.freeze raise SyntaxError, parse_context.locale.t('errors.syntax.unexpected_else',
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_else".freeze, block_name: block_name)
block_name: block_name)) elsif tag.start_with?('end')
when 'end'.freeze raise SyntaxError, parse_context.locale.t('errors.syntax.invalid_delimiter',
raise SyntaxError.new(parse_context.locale.t("errors.syntax.invalid_delimiter".freeze, tag: tag,
block_name: block_name, block_name: block_name,
block_delimiter: block_delimiter)) block_delimiter: block_delimiter)
else else
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unknown_tag".freeze, tag: tag)) raise SyntaxError, parse_context.locale.t('errors.syntax.unknown_tag', tag: tag)
end end
end end
@@ -50,18 +53,15 @@ module Liquid
protected protected
def parse_body(body, tokens) def parse_body(body, tokens)
if parse_context.depth >= MAX_DEPTH raise StackLevelError, 'Nesting too deep' if parse_context.depth >= MAX_DEPTH
raise StackLevelError, "Nesting too deep".freeze
end
parse_context.depth += 1 parse_context.depth += 1
begin begin
body.parse(tokens, parse_context) do |end_tag_name, end_tag_params| body.parse(tokens, parse_context) do |end_tag_name, end_tag_params|
@blank &&= body.blank? @blank &&= body.blank?
return false if end_tag_name == block_delimiter return false if end_tag_name == block_delimiter
unless end_tag_name raise SyntaxError, parse_context.locale.t('errors.syntax.tag_never_closed', block_name: block_name) unless end_tag_name
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
end
# this tag is not registered with the system # this tag is not registered with the system
# pass it to the current block for special handling or error reporting # pass it to the current block for special handling or error reporting

View File

@@ -1,9 +1,12 @@
# frozen_string_literal: true
module Liquid module Liquid
class BlockBody class BlockBody
FullToken = /\A#{TagStart}#{WhitespaceControl}?\s*(\w+)\s*(.*?)#{WhitespaceControl}?#{TagEnd}\z/om FULL_TOKEN = /\A#{TAG_START}#{WHITESPACE_CONTROL}?\s*(\w+)\s*(.*?)#{WHITESPACE_CONTROL}?#{TAG_END}\z/om.freeze
ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om CONTENT_OF_VARIABLE = /\A#{VARIABLE_START}#{WHITESPACE_CONTROL}?(.*?)#{WHITESPACE_CONTROL}?#{VARIABLE_END}\z/om.freeze
TAGSTART = "{%".freeze WHITESPACE_OR_NOTHING = /\A\s*\z/.freeze
VARSTART = "{{".freeze TAG_START = '{%'
VAR_START = '{{'
attr_reader :nodelist attr_reader :nodelist
@@ -14,39 +17,34 @@ module Liquid
def parse(tokenizer, parse_context) def parse(tokenizer, parse_context)
parse_context.line_number = tokenizer.line_number parse_context.line_number = tokenizer.line_number
while token = tokenizer.shift while (token = tokenizer.shift)
unless token.empty? next if token.empty?
case
when token.start_with?(TAGSTART) case
whitespace_handler(token, parse_context) when token.start_with?(TAG_START)
if token =~ FullToken whitespace_handler(token, parse_context)
tag_name = $1 raise_missing_tag_terminator(token, parse_context) unless token =~ FULL_TOKEN
markup = $2 tag_name = Regexp.last_match(1)
# fetch the tag from registered blocks markup = Regexp.last_match(2)
if tag = registered_tags[tag_name] # fetch the tag from registered blocks
new_tag = tag.parse(tag_name, markup, tokenizer, parse_context) unless (tag = registered_tags[tag_name])
@blank &&= new_tag.blank? # end parsing if we reach an unknown tag and let the caller decide
@nodelist << new_tag # determine how to proceed
else return yield tag_name, markup
# end parsing if we reach an unknown tag and let the caller decide
# determine how to proceed
return yield tag_name, markup
end
else
raise_missing_tag_terminator(token, parse_context)
end
when token.start_with?(VARSTART)
whitespace_handler(token, parse_context)
@nodelist << create_variable(token, parse_context)
@blank = false
else
if parse_context.trim_whitespace
token.lstrip!
end
parse_context.trim_whitespace = false
@nodelist << token
@blank &&= !!(token =~ /\A\s*\z/)
end end
new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
@blank &&= new_tag.blank?
@nodelist << new_tag
when token.start_with?(VAR_START)
whitespace_handler(token, parse_context)
@nodelist << create_variable(token, parse_context)
@blank = false
else
token.lstrip! if parse_context.trim_whitespace
parse_context.trim_whitespace = false
@nodelist << token
@blank &&= token.match?(WHITESPACE_OR_NOTHING)
end end
parse_context.line_number = tokenizer.line_number parse_context.line_number = tokenizer.line_number
end end
@@ -55,13 +53,11 @@ module Liquid
end end
def whitespace_handler(token, parse_context) def whitespace_handler(token, parse_context)
if token[2] == WhitespaceControl if token[2] == WHITESPACE_CONTROL
previous_token = @nodelist.last previous_token = @nodelist.last
if previous_token.is_a? String previous_token.rstrip! if previous_token.is_a?(String)
previous_token.rstrip!
end
end end
parse_context.trim_whitespace = (token[-3] == WhitespaceControl) parse_context.trim_whitespace = (token[-3] == WHITESPACE_CONTROL)
end end
def blank? def blank?
@@ -69,56 +65,62 @@ module Liquid
end end
def render(context) def render(context)
output = [] render_to_output_buffer(context, +'')
end
def render_to_output_buffer(context, output)
context.resource_limits.render_score += @nodelist.length context.resource_limits.render_score += @nodelist.length
@nodelist.each do |token| idx = 0
# Break out if we have any unhanded interrupts. while (node = @nodelist[idx])
break if context.interrupt? previous_output_size = output.bytesize
begin case node
when String
output << node
when Variable
render_node(context, output, node)
when Block
render_node(context, node.blank? ? +'' : output, node)
break if context.interrupt? # might have happened in a for-block
when Continue, Break
# If we get an Interrupt that means the block must stop processing. An # If we get an Interrupt that means the block must stop processing. An
# Interrupt is any command that stops block execution such as {% break %} # Interrupt is any command that stops block execution such as {% break %}
# or {% continue %} # or {% continue %}
if token.is_a?(Continue) || token.is_a?(Break) context.push_interrupt(node.interrupt)
context.push_interrupt(token.interrupt) break
break else # Other non-Block tags
end render_node(context, output, node)
break if context.interrupt? # might have happened through an include
node_output = render_node(token, context)
unless token.is_a?(Block) && token.blank?
output << node_output
end
rescue MemoryError => e
raise e
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
context.handle_error(e, token.line_number)
output << nil
rescue ::StandardError => e
line_number = token.is_a?(String) ? nil : token.line_number
output << context.handle_error(e, line_number)
end end
idx += 1
raise_if_resource_limits_reached(context, output.bytesize - previous_output_size)
end end
output.join output
end end
private private
def render_node(node, context) def render_node(context, output, node)
node_output = node.is_a?(String) ? node : node.render(context) node.render_to_output_buffer(context, output)
node_output = node_output.is_a?(Array) ? node_output.join : node_output.to_s rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
context.handle_error(e, node.line_number)
rescue ::StandardError => e
line_number = node.is_a?(String) ? nil : node.line_number
output << context.handle_error(e, line_number)
end
context.resource_limits.render_length += node_output.length def raise_if_resource_limits_reached(context, length)
if context.resource_limits.reached? context.resource_limits.render_length += length
raise MemoryError.new("Memory limits exceeded".freeze) return unless context.resource_limits.reached?
end
node_output raise MemoryError, 'Memory limits exceeded'
end end
def create_variable(token, parse_context) def create_variable(token, parse_context)
token.scan(ContentOfVariable) do |content| token.scan(CONTENT_OF_VARIABLE) do |content|
markup = content.first markup = content.first
return Variable.new(markup, parse_context) return Variable.new(markup, parse_context)
end end
@@ -126,11 +128,11 @@ module Liquid
end end
def raise_missing_tag_terminator(token, parse_context) def raise_missing_tag_terminator(token, parse_context)
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_termination".freeze, token: token, tag_end: TagEnd.inspect)) raise SyntaxError, parse_context.locale.t('errors.syntax.tag_termination', token: token, tag_end: TAG_END.inspect)
end end
def raise_missing_variable_terminator(token, parse_context) def raise_missing_variable_terminator(token, parse_context)
raise SyntaxError.new(parse_context.locale.t("errors.syntax.variable_termination".freeze, token: token, tag_end: VariableEnd.inspect)) raise SyntaxError, parse_context.locale.t('errors.syntax.variable_termination', token: token, tag_end: VARIABLE_END.inspect)
end end
def registered_tags def registered_tags

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
# Container for liquid nodes which conveniently wraps decision making logic # Container for liquid nodes which conveniently wraps decision making logic
# #
@@ -7,36 +9,36 @@ module Liquid
# c.evaluate #=> true # c.evaluate #=> true
# #
class Condition #:nodoc: class Condition #:nodoc:
@@operators = { @operators = {
'=='.freeze => ->(cond, left, right) { cond.send(:equal_variables, left, right) }, '==' => ->(cond, left, right) { cond.send(:equal_variables, left, right) },
'!='.freeze => ->(cond, left, right) { !cond.send(:equal_variables, left, right) }, '!=' => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
'<>'.freeze => ->(cond, left, right) { !cond.send(:equal_variables, left, right) }, '<>' => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
'<'.freeze => :<, '<' => :<,
'>'.freeze => :>, '>' => :>,
'>='.freeze => :>=, '>=' => :>=,
'<='.freeze => :<=, '<=' => :<=,
'contains'.freeze => lambda do |cond, left, right| 'contains' => lambda do |_cond, left, right|
if left && right && left.respond_to?(:include?) if left && right && left.respond_to?(:include?)
right = right.to_s if left.is_a?(String) right = right.to_s if left.is_a?(String)
left.include?(right) left.include?(right)
else else
false false
end end
end end,
} }
def self.operators class << self
@@operators attr_accessor :operators
end end
attr_reader :attachment attr_reader :attachment, :child_condition
attr_accessor :left, :operator, :right attr_accessor :left, :operator, :right
def initialize(left = nil, operator = nil, right = nil) def initialize(left = nil, operator = nil, right = nil)
@left = left @left = left
@operator = operator @operator = operator
@right = right @right = right
@child_relation = nil @child_relation = nil
@child_condition = nil @child_condition = nil
end end
@@ -78,12 +80,12 @@ module Liquid
end end
def inspect def inspect
"#<Condition #{[@left, @operator, @right].compact.join(' '.freeze)}>" "#<Condition #{[@left, @operator, @right].compact.join(' ')}>"
end end
protected protected
attr_reader :child_relation, :child_condition attr_reader :child_relation
private private
@@ -116,7 +118,7 @@ module Liquid
left = context.evaluate(left) left = context.evaluate(left)
right = context.evaluate(right) right = context.evaluate(right)
operation = self.class.operators[op] || raise(Liquid::ArgumentError.new("Unknown operator #{op}")) operation = self.class.operators[op] || raise(Liquid::ArgumentError, "Unknown operator #{op}")
if operation.respond_to?(:call) if operation.respond_to?(:call)
operation.call(self, left, right) operation.call(self, left, right)
@@ -124,10 +126,19 @@ module Liquid
begin begin
left.send(operation, right) left.send(operation, right)
rescue ::ArgumentError => e rescue ::ArgumentError => e
raise Liquid::ArgumentError.new(e.message) raise Liquid::ArgumentError, e.message
end end
end end
end end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[
@node.left, @node.right,
@node.child_condition, @node.attachment
].compact
end
end
end end
class ElseCondition < Condition class ElseCondition < Condition

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
# Context keeps the variable stack and resolves variables, as well as keywords # Context keeps the variable stack and resolves variables, as well as keywords
# #
@@ -28,9 +30,7 @@ module Liquid
@this_stack_used = false @this_stack_used = false
self.exception_renderer = Template.default_exception_renderer self.exception_renderer = Template.default_exception_renderer
if rethrow_errors self.exception_renderer = ->(_e) { raise } if rethrow_errors
self.exception_renderer = ->(e) { raise }
end
@interrupts = [] @interrupts = []
@filters = [] @filters = []
@@ -89,7 +89,7 @@ module Liquid
# Push new local scope on the stack. use <tt>Context#stack</tt> instead # Push new local scope on the stack. use <tt>Context#stack</tt> instead
def push(new_scope = {}) def push(new_scope = {})
@scopes.unshift(new_scope) @scopes.unshift(new_scope)
raise StackLevelError, "Nesting too deep".freeze if @scopes.length > Block::MAX_DEPTH raise StackLevelError, 'Nesting too deep' if @scopes.length > Block::MAX_DEPTH
end end
# Merge a hash of variables in the current local scope # Merge a hash of variables in the current local scope
@@ -100,6 +100,7 @@ module Liquid
# Pop from the stack. use <tt>Context#stack</tt> instead # Pop from the stack. use <tt>Context#stack</tt> instead
def pop def pop
raise ContextError if @scopes.size == 1 raise ContextError if @scopes.size == 1
@scopes.shift @scopes.shift
end end
@@ -171,7 +172,9 @@ module Liquid
if scope.nil? if scope.nil?
@environments.each do |e| @environments.each do |e|
variable = lookup_and_evaluate(e, key, raise_on_not_found: raise_on_not_found) variable = lookup_and_evaluate(e, key, raise_on_not_found: raise_on_not_found)
unless variable.nil? # When lookup returned a value OR there is no value but the lookup also did not raise
# then it is the value we are looking for.
if !variable.nil? || @strict_variables && raise_on_not_found
scope = e scope = e
break break
end end
@@ -188,14 +191,12 @@ module Liquid
end end
def lookup_and_evaluate(obj, key, raise_on_not_found: true) def lookup_and_evaluate(obj, key, raise_on_not_found: true)
if @strict_variables && raise_on_not_found && obj.respond_to?(:key?) && !obj.key?(key) raise Liquid::UndefinedVariable, "undefined variable #{key}" if @strict_variables && raise_on_not_found && obj.respond_to?(:key?) && !obj.key?(key)
raise Liquid::UndefinedVariable, "undefined variable #{key}"
end
value = obj[key] value = obj[key]
if value.is_a?(Proc) && obj.respond_to?(:[]=) if value.is_a?(Proc) && obj.respond_to?(:[]=)
obj[key] = (value.arity == 0) ? value.call : value.call(self) obj[key] = value.arity == 0 ? value.call : value.call(self)
else else
value value
end end
@@ -206,8 +207,8 @@ module Liquid
def internal_error def internal_error
# raise and catch to set backtrace and cause on exception # raise and catch to set backtrace and cause on exception
raise Liquid::InternalError, 'internal' raise Liquid::InternalError, 'internal'
rescue Liquid::InternalError => exc rescue Liquid::InternalError => e
exc e
end end
def squash_instance_assigns_with_environments def squash_instance_assigns_with_environments

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
class Document < BlockBody class Document < BlockBody
def self.parse(tokens, parse_context) def self.parse(tokens, parse_context)
@@ -7,7 +9,7 @@ module Liquid
end end
def parse(tokens, parse_context) def parse(tokens, parse_context)
super do |end_tag_name, end_tag_params| super do |end_tag_name, _end_tag_params|
unknown_tag(end_tag_name, parse_context) if end_tag_name unknown_tag(end_tag_name, parse_context) if end_tag_name
end end
rescue SyntaxError => e rescue SyntaxError => e
@@ -17,10 +19,10 @@ module Liquid
def unknown_tag(tag, parse_context) def unknown_tag(tag, parse_context)
case tag case tag
when 'else'.freeze, 'end'.freeze when 'else', 'end'
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_outer_tag".freeze, tag: tag)) raise SyntaxError, parse_context.locale.t('errors.syntax.unexpected_outer_tag', tag: tag)
else else
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unknown_tag".freeze, tag: tag)) raise SyntaxError, parse_context.locale.t('errors.syntax.unknown_tag', tag: tag)
end end
end end
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'set' require 'set'
module Liquid module Liquid
@@ -25,7 +27,8 @@ module Liquid
# Catch all for the method # Catch all for the method
def liquid_method_missing(method) def liquid_method_missing(method)
return nil unless @context && @context.strict_variables return nil unless @context&.strict_variables
raise Liquid::UndefinedDropMethod, "undefined method #{method}" raise Liquid::UndefinedDropMethod, "undefined method #{method}"
end end
@@ -67,7 +70,7 @@ module Liquid
if include?(Enumerable) if include?(Enumerable)
blacklist += Enumerable.public_instance_methods blacklist += Enumerable.public_instance_methods
blacklist -= [:sort, :count, :first, :min, :max, :include?] blacklist -= %i[sort count first min max include?]
end end
whitelist = [:to_liquid] + (public_instance_methods - blacklist) whitelist = [:to_liquid] + (public_instance_methods - blacklist)

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
class Error < ::StandardError class Error < ::StandardError
attr_accessor :line_number attr_accessor :line_number
@@ -5,12 +7,12 @@ module Liquid
attr_accessor :markup_context attr_accessor :markup_context
def to_s(with_prefix = true) def to_s(with_prefix = true)
str = "" str = +''
str << message_prefix if with_prefix str << message_prefix if with_prefix
str << super() str << super()
if markup_context if markup_context
str << " " str << ' '
str << markup_context str << markup_context
end end
@@ -20,20 +22,20 @@ module Liquid
private private
def message_prefix def message_prefix
str = "" str = +''
if is_a?(SyntaxError) str << if is_a?(SyntaxError)
str << "Liquid syntax error" 'Liquid syntax error'
else else
str << "Liquid error" 'Liquid error'
end end
if line_number if line_number
str << " (" str << ' ('
str << template_name << " " if template_name str << template_name << ' ' if template_name
str << "line " << line_number.to_s << ")" str << 'line ' << line_number.to_s << ')'
end end
str << ": " str << ': '
str str
end end
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
class Expression class Expression
class MethodLiteral class MethodLiteral
@@ -14,28 +16,32 @@ module Liquid
end end
LITERALS = { LITERALS = {
nil => nil, 'nil'.freeze => nil, 'null'.freeze => nil, ''.freeze => nil, nil => nil, 'nil' => nil, 'null' => nil, '' => nil,
'true'.freeze => true, 'true' => true,
'false'.freeze => false, 'false' => false,
'blank'.freeze => MethodLiteral.new(:blank?, '').freeze, 'blank' => MethodLiteral.new(:blank?, '').freeze,
'empty'.freeze => MethodLiteral.new(:empty?, '').freeze 'empty' => MethodLiteral.new(:empty?, '').freeze
} }.freeze
SINGLE_QUOTED_STRING = /\A'(.*)'\z/m.freeze
DOUBLE_QUOTED_STRING = /\A"(.*)"\z/m.freeze
INTEGERS_REGEX = /\A(-?\d+)\z/.freeze
FLOATS_REGEX = /\A(-?\d[\d\.]+)\z/.freeze
RANGES_REGEX = /\A\((\S+)\.\.(\S+)\)\z/.freeze
def self.parse(markup) def self.parse(markup)
if LITERALS.key?(markup) if LITERALS.key?(markup)
LITERALS[markup] LITERALS[markup]
else else
case markup case markup
when /\A'(.*)'\z/m # Single quoted strings when SINGLE_QUOTED_STRING, DOUBLE_QUOTED_STRING
$1 Regexp.last_match(1)
when /\A"(.*)"\z/m # Double quoted strings when INTEGERS_REGEX
$1 Regexp.last_match(1).to_i
when /\A(-?\d+)\z/ # Integer and floats when RANGES_REGEX
$1.to_i RangeLookup.parse(Regexp.last_match(1), Regexp.last_match(2))
when /\A\((\S+)\.\.(\S+)\)\z/ # Ranges when FLOATS_REGEX
RangeLookup.parse($1, $2) Regexp.last_match(1).to_f
when /\A(-?\d[\d\.]+)\z/ # Floats
$1.to_f
else else
VariableLookup.parse(markup) VariableLookup.parse(markup)
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'time' require 'time'
require 'date' require 'date'
@@ -7,6 +9,12 @@ class String # :nodoc:
end end
end end
class Symbol # :nodoc:
def to_liquid
to_s
end
end
class Array # :nodoc: class Array # :nodoc:
def to_liquid def to_liquid
self self

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
# A Liquid file system is a way to let your templates retrieve other templates for use with the include tag. # A Liquid file system is a way to let your templates retrieve other templates for use with the include tag.
# #
@@ -15,7 +17,7 @@ module Liquid
class BlankFileSystem class BlankFileSystem
# Called by Liquid to retrieve a template file # Called by Liquid to retrieve a template file
def read_template_file(_template_path) def read_template_file(_template_path)
raise FileSystemError, "This liquid context does not allow includes." raise FileSystemError, 'This liquid context does not allow includes.'
end end
end end
@@ -44,7 +46,7 @@ module Liquid
class LocalFileSystem class LocalFileSystem
attr_accessor :root attr_accessor :root
def initialize(root, pattern = "_%s.liquid".freeze) def initialize(root, pattern = '_%s.liquid')
@root = root @root = root
@pattern = pattern @pattern = pattern
end end
@@ -57,9 +59,9 @@ module Liquid
end end
def full_path(template_path) def full_path(template_path)
raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ /\A[^.\/][a-zA-Z0-9_\/]+\z/ raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ %r{\A[^./][a-zA-Z0-9_/]+\z}
full_path = if template_path.include?('/'.freeze) full_path = if template_path.include?('/')
File.join(root, File.dirname(template_path), @pattern % File.basename(template_path)) File.join(root, File.dirname(template_path), @pattern % File.basename(template_path))
else else
File.join(root, @pattern % template_path) File.join(root, @pattern % template_path)

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
class ForloopDrop < Drop class ForloopDrop < Drop
def initialize(name, length, parentloop) def initialize(name, length, parentloop)

View File

@@ -1,8 +1,10 @@
# frozen_string_literal: true
require 'yaml' require 'yaml'
module Liquid module Liquid
class I18n class I18n
DEFAULT_LOCALE = File.join(File.expand_path(__dir__), "locales", "en.yml") DEFAULT_LOCALE = File.join(File.expand_path(__dir__), 'locales', 'en.yml')
TranslationError = Class.new(StandardError) TranslationError = Class.new(StandardError)
@@ -26,13 +28,13 @@ module Liquid
def interpolate(name, vars) def interpolate(name, vars)
name.gsub(/%\{(\w+)\}/) do name.gsub(/%\{(\w+)\}/) do
# raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym] # raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym]
"#{vars[$1.to_sym]}" (vars[Regexp.last_match(1).to_sym]).to_s
end end
end end
def deep_fetch_translation(name) def deep_fetch_translation(name)
name.split('.'.freeze).reduce(locale) do |level, cur| name.split('.').reduce(locale) do |level, cur|
level[cur] or raise TranslationError, "Translation for #{name} does not exist in locale #{path}" level[cur] || raise(TranslationError, "Translation for #{name} does not exist in locale #{path}")
end end
end end
end end

View File

@@ -1,16 +1,18 @@
# frozen_string_literal: true
module Liquid module Liquid
# An interrupt is any command that breaks processing of a block (ex: a for loop). # An interrupt is any command that breaks processing of a block (ex: a for loop).
class Interrupt class Interrupt
attr_reader :message attr_reader :message
def initialize(message = nil) def initialize(message = nil)
@message = message || "interrupt".freeze @message = message || 'interrupt'
end end
end end
# Interrupt that is thrown whenever a {% break %} is called. # Interrupt that is thrown whenever a {% break %} is called.
class BreakInterrupt < Interrupt; end class BreakInterrupt < RuntimeError; end
# Interrupt that is thrown whenever a {% continue %} is called. # Interrupt that is thrown whenever a {% continue %} is called.
class ContinueInterrupt < Interrupt; end class ContinueInterrupt < RuntimeError; end
end end

79
lib/liquid/legacy.rb Normal file
View File

@@ -0,0 +1,79 @@
# frozen_string_literal: true
module Liquid
FilterSeparator = FILTER_SEPARATOR
ArgumentSeparator = ARGUMENT_SEPARATOR
FilterArgumentSeparator = FILTER_ARGUMENT_SEPARATOR
VariableAttributeSeparator = VARIABLE_ATTRIBUTE_SEPARATOR
WhitespaceControl = WHITESPACE_CONTROL
TagStart = TAG_START
TagEnd = TAG_END
VariableSignature = VARIABLE_SIGNATURE
VariableSegment = VARIABLE_SEGMENT
VariableStart = VARIABLE_START
VariableEnd = VARIABLE_END
VariableIncompleteEnd = VARIABLE_INCOMPLETE_END
QuotedString = QUOTED_STRING
QuotedFragment = QUOTED_FRAGMENT
TagAttributes = TAG_ATTRIBUTES
AnyStartingTag = ANY_STARTING_TAG
PartialTemplateParser = PARTIAL_TEMPLATE_PARSER
TemplateParser = TEMPLATE_PARSER
VariableParser = VARIABLE_PARSER
class BlockBody
FullToken = FULL_TOKEN
ContentOfVariable = CONTENT_OF_VARIABLE
WhitespaceOrNothing = WHITESPACE_OR_NOTHING
TAGSTART = TAG_START
VARSTART = VAR_START
end
class Assign < Tag
Syntax = SYNTAX
end
class Capture < Block
Syntax = SYNTAX
end
class Case < Block
Syntax = SYNTAX
WhenSyntax = WHEN_SYNTAX
end
class Cycle < Tag
SimpleSyntax = SIMPLE_SYNTAX
NamedSyntax = NAMED_SYNTAX
end
class For < Block
Syntax = SYNTAX
end
class If < Block
Syntax = SYNTAX
ExpressionsAndOperators = EXPRESSIONS_AND_OPERATORS
end
class Include < Tag
Syntax = SYNTAX
end
class Raw < Block
Syntax = SYNTAX
FullTokenPossiblyInvalid = FULL_TOKEN_POSSIBLY_INVALID
end
class TableRow < Block
Syntax = SYNTAX
end
class Variable
FilterMarkupRegex = FILTER_MARKUP_REGEX
FilterParser = FILTER_PARSER
FilterArgsRegex = FILTER_ARGS_REGEX
JustTagAttributes = JUST_TAG_ATTRIBUTES
MarkupWithQuotedFragment = MARKUP_WITH_QUOTED_FRAGMENT
end
end

View File

@@ -1,24 +1,27 @@
require "strscan" # frozen_string_literal: true
require 'strscan'
module Liquid module Liquid
class Lexer class Lexer
SPECIALS = { SPECIALS = {
'|'.freeze => :pipe, '|' => :pipe,
'.'.freeze => :dot, '.' => :dot,
':'.freeze => :colon, ':' => :colon,
','.freeze => :comma, ',' => :comma,
'['.freeze => :open_square, '[' => :open_square,
']'.freeze => :close_square, ']' => :close_square,
'('.freeze => :open_round, '(' => :open_round,
')'.freeze => :close_round, ')' => :close_round,
'?'.freeze => :question, '?' => :question,
'-'.freeze => :dash '-' => :dash,
} }.freeze
IDENTIFIER = /[a-zA-Z_][\w-]*\??/ IDENTIFIER = /[a-zA-Z_][\w-]*\??/.freeze
SINGLE_STRING_LITERAL = /'[^\']*'/ SINGLE_STRING_LITERAL = /'[^\']*'/.freeze
DOUBLE_STRING_LITERAL = /"[^\"]*"/ DOUBLE_STRING_LITERAL = /"[^\"]*"/.freeze
NUMBER_LITERAL = /-?\d+(\.\d+)?/ NUMBER_LITERAL = /-?\d+(\.\d+)?/.freeze
DOTDOT = /\.\./ DOTDOT = /\.\./.freeze
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/ COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/.freeze
WHITESPACE_OR_NOTHING = /\s*/.freeze
def initialize(input) def initialize(input)
@ss = StringScanner.new(input) @ss = StringScanner.new(input)
@@ -28,18 +31,18 @@ module Liquid
@output = [] @output = []
until @ss.eos? until @ss.eos?
@ss.skip(/\s*/) @ss.skip(WHITESPACE_OR_NOTHING)
break if @ss.eos? break if @ss.eos?
tok = case
when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t] tok = if (t = @ss.scan(COMPARISON_OPERATOR)) then [:comparison, t]
when t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t] elsif (t = @ss.scan(SINGLE_STRING_LITERAL)) then [:string, t]
when t = @ss.scan(DOUBLE_STRING_LITERAL) then [:string, t] elsif (t = @ss.scan(DOUBLE_STRING_LITERAL)) then [:string, t]
when t = @ss.scan(NUMBER_LITERAL) then [:number, t] elsif (t = @ss.scan(NUMBER_LITERAL)) then [:number, t]
when t = @ss.scan(IDENTIFIER) then [:id, t] elsif (t = @ss.scan(IDENTIFIER)) then [:id, t]
when t = @ss.scan(DOTDOT) then [:dotdot, t] elsif (t = @ss.scan(DOTDOT)) then [:dotdot, t]
else else
c = @ss.getch c = @ss.getch
if s = SPECIALS[c] if (s = SPECIALS[c])
[s, c] [s, c]
else else
raise SyntaxError, "Unexpected character #{c}" raise SyntaxError, "Unexpected character #{c}"

View File

@@ -14,7 +14,7 @@
if: "Syntax Error in tag 'if' - Valid syntax: if [expression]" if: "Syntax Error in tag 'if' - Valid syntax: if [expression]"
include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]" include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]"
unknown_tag: "Unknown tag '%{tag}'" unknown_tag: "Unknown tag '%{tag}'"
invalid_delimiter: "'end' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}" invalid_delimiter: "'%{tag}' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
unexpected_else: "%{block_name} tag does not expect 'else' tag" unexpected_else: "%{block_name} tag does not expect 'else' tag"
unexpected_outer_tag: "Unexpected outer '%{tag}' tag" unexpected_outer_tag: "Unexpected outer '%{tag}' tag"
tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}" tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
class ParseContext class ParseContext
attr_accessor :locale, :line_number, :trim_whitespace, :depth attr_accessor :locale, :line_number, :trim_whitespace, :depth
@@ -19,7 +21,6 @@ module Liquid
@partial = value @partial = value
@options = value ? partial_options : @template_options @options = value ? partial_options : @template_options
@error_mode = @options[:error_mode] || Template.error_mode @error_mode = @options[:error_mode] || Template.error_mode
value
end end
def partial_options def partial_options
@@ -28,7 +29,7 @@ module Liquid
if dont_pass == true if dont_pass == true
{ locale: locale } { locale: locale }
elsif dont_pass.is_a?(Array) elsif dont_pass.is_a?(Array)
@template_options.reject { |k, v| dont_pass.include?(k) } @template_options.reject { |k, _v| dont_pass.include?(k) }
else else
@template_options @template_options
end end

View File

@@ -0,0 +1,42 @@
# frozen_string_literal: true
module Liquid
class ParseTreeVisitor
def self.for(node, callbacks = Hash.new(proc {}))
if defined?(node.class::ParseTreeVisitor)
node.class::ParseTreeVisitor
else
self
end.new(node, callbacks)
end
def initialize(node, callbacks)
@node = node
@callbacks = callbacks
end
def add_callback_for(*classes, &block)
callback = block
callback = ->(node, _) { yield node } if block.arity.abs == 1
callback = ->(_, _) { yield } if block.arity.zero?
classes.each { |klass| @callbacks[klass] = callback }
self
end
def visit(context = nil)
children.map do |node|
item, new_context = @callbacks[node.class].call(node, context)
[
item,
ParseTreeVisitor.for(node, @callbacks).visit(new_context || context),
]
end
end
protected
def children
@node.respond_to?(:nodelist) ? Array(@node.nodelist) : []
end
end
end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
class Parser class Parser
def initialize(input) def initialize(input)
@@ -12,9 +14,8 @@ module Liquid
def consume(type = nil) def consume(type = nil)
token = @tokens[@p] token = @tokens[@p]
if type && token[0] != type raise SyntaxError, "Expected #{type} but found #{@tokens[@p].first}" if type && token[0] != type
raise SyntaxError, "Expected #{type} but found #{@tokens[@p].first}"
end
@p += 1 @p += 1
token[1] token[1]
end end
@@ -25,6 +26,7 @@ module Liquid
def consume?(type) def consume?(type)
token = @tokens[@p] token = @tokens[@p]
return false unless token && token[0] == type return false unless token && token[0] == type
@p += 1 @p += 1
token[1] token[1]
end end
@@ -34,6 +36,7 @@ module Liquid
token = @tokens[@p] token = @tokens[@p]
return false unless token && token[0] == :id return false unless token && token[0] == :id
return false unless token[1] == str return false unless token[1] == str
@p += 1 @p += 1
token[1] token[1]
end end
@@ -41,14 +44,18 @@ module Liquid
def look(type, ahead = 0) def look(type, ahead = 0)
tok = @tokens[@p + ahead] tok = @tokens[@p + ahead]
return false unless tok return false unless tok
tok[0] == type tok[0] == type
end end
SINGLE_TOKEN_EXPRESSION_TYPES = %i[string number].freeze
private_constant :SINGLE_TOKEN_EXPRESSION_TYPES
def expression def expression
token = @tokens[@p] token = @tokens[@p]
if token[0] == :id if token[0] == :id
variable_signature variable_signature
elsif [:string, :number].include? token[0] elsif SINGLE_TOKEN_EXPRESSION_TYPES.include?(token[0])
consume consume
elsif token.first == :open_round elsif token.first == :open_round
consume consume
@@ -63,11 +70,9 @@ module Liquid
end end
def argument def argument
str = "" str = +''
# might be a keyword argument (identifier: expression) # might be a keyword argument (identifier: expression)
if look(:id) && look(:colon, 1) str << consume << consume << ' ' if look(:id) && look(:colon, 1)
str << consume << consume << ' '.freeze
end
str << expression str << expression
str str

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
module ParserSwitching module ParserSwitching
def parse_with_selected_parser(markup) def parse_with_selected_parser(markup)

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'liquid/profiler/hooks' require 'liquid/profiler/hooks'
module Liquid module Liquid
@@ -97,9 +99,9 @@ module Liquid
end end
def initialize def initialize
@partial_stack = ["<root>"] @partial_stack = ['<root>']
@root_timing = Timing.new("", current_partial) @root_timing = Timing.new('', current_partial)
@timing_stack = [@root_timing] @timing_stack = [@root_timing]
@render_start_at = Time.now @render_start_at = Time.now

View File

@@ -1,8 +1,10 @@
# frozen_string_literal: true
module Liquid module Liquid
class BlockBody class BlockBody
def render_node_with_profiling(node, context) def render_node_with_profiling(context, output, node)
Profiler.profile_node_render(node) do Profiler.profile_node_render(node) do
render_node_without_profiling(node, context) render_node_without_profiling(context, output, node)
end end
end end
@@ -11,13 +13,13 @@ module Liquid
end end
class Include < Tag class Include < Tag
def render_with_profiling(context) def render_to_output_buffer_with_profiling(context, output)
Profiler.profile_children(context.evaluate(@template_name_expr).to_s) do Profiler.profile_children(context.evaluate(@template_name_expr).to_s) do
render_without_profiling(context) render_to_output_buffer_without_profiling(context, output)
end end
end end
alias_method :render_without_profiling, :render alias_method :render_to_output_buffer_without_profiling, :render_to_output_buffer
alias_method :render, :render_with_profiling alias_method :render_to_output_buffer, :render_to_output_buffer_with_profiling
end end
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
class RangeLookup class RangeLookup
def self.parse(start_markup, end_markup) def self.parse(start_markup, end_markup)

View File

@@ -1,7 +1,9 @@
# frozen_string_literal: true
module Liquid module Liquid
class ResourceLimits class ResourceLimits
attr_accessor :render_length, :render_score, :assign_score, attr_accessor :render_length, :render_score, :assign_score,
:render_length_limit, :render_score_limit, :assign_score_limit :render_length_limit, :render_score_limit, :assign_score_limit
def initialize(limits) def initialize(limits)
@render_length_limit = limits[:render_length_limit] @render_length_limit = limits[:render_length_limit]

View File

@@ -1,16 +1,24 @@
# frozen_string_literal: true
require 'cgi' require 'cgi'
require 'bigdecimal' require 'bigdecimal'
module Liquid module Liquid
module StandardFilters module StandardFilters
HTML_ESCAPE = { HTML_ESCAPE = {
'&'.freeze => '&amp;'.freeze, '&' => '&amp;',
'>'.freeze => '&gt;'.freeze, '>' => '&gt;',
'<'.freeze => '&lt;'.freeze, '<' => '&lt;',
'"'.freeze => '&quot;'.freeze, '"' => '&quot;',
"'".freeze => '&#39;'.freeze "'" => '&#39;',
} }.freeze
HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/ HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/.freeze
STRIP_HTML_BLOCKS = Regexp.union(
%r{<script.*?</script>}m,
/<!--.*?-->/m,
%r{<style.*?</style>}m
)
STRIP_HTML_TAGS = /<.*?>/m.freeze
# Return the size of an array or of an string # Return the size of an array or of an string
def size(input) def size(input)
@@ -46,7 +54,12 @@ module Liquid
end end
def url_decode(input) def url_decode(input)
CGI.unescape(input.to_s) unless input.nil? return if input.nil?
result = CGI.unescape(input.to_s)
raise Liquid::ArgumentError, "invalid byte sequence in #{result.encoding}" unless result.valid_encoding?
result
end end
def slice(input, offset, length = nil) def slice(input, offset, length = nil)
@@ -61,23 +74,25 @@ module Liquid
end end
# Truncate a string down to x characters # Truncate a string down to x characters
def truncate(input, length = 50, truncate_string = "...".freeze) def truncate(input, length = 50, truncate_string = '...')
return if input.nil? return if input.nil?
input_str = input.to_s input_str = input.to_s
length = Utils.to_integer(length) length = Utils.to_integer(length)
truncate_string_str = truncate_string.to_s truncate_string_str = truncate_string.to_s
l = length - truncate_string_str.length l = length - truncate_string_str.length
l = 0 if l < 0 l = 0 if l < 0
input_str.length > length ? input_str[0...l] + truncate_string_str : input_str input_str.length > length ? input_str[0...l].concat(truncate_string_str) : input_str
end end
def truncatewords(input, words = 15, truncate_string = "...".freeze) def truncatewords(input, words = 15, truncate_string = '...')
return if input.nil? return if input.nil?
wordlist = input.to_s.split wordlist = input.to_s.split
words = Utils.to_integer(words) words = Utils.to_integer(words)
l = words - 1 l = words - 1
l = 0 if l < 0 l = 0 if l < 0
wordlist.length > l ? wordlist[0..l].join(" ".freeze) + truncate_string.to_s : input wordlist.length > l ? wordlist[0..l].join(' ').concat(truncate_string.to_s) : input
end end
# Split input string into an array of substrings separated by given pattern. # Split input string into an array of substrings separated by given pattern.
@@ -102,17 +117,19 @@ module Liquid
end end
def strip_html(input) def strip_html(input)
empty = ''.freeze empty = ''
input.to_s.gsub(/<script.*?<\/script>/m, empty).gsub(/<!--.*?-->/m, empty).gsub(/<style.*?<\/style>/m, empty).gsub(/<.*?>/m, empty) result = input.to_s.gsub(STRIP_HTML_BLOCKS, empty)
result.gsub!(STRIP_HTML_TAGS, empty)
result
end end
# Remove all newlines from the string # Remove all newlines from the string
def strip_newlines(input) def strip_newlines(input)
input.to_s.gsub(/\r?\n/, ''.freeze) input.to_s.gsub(/\r?\n/, '')
end end
# Join elements of the array with certain character between them # Join elements of the array with certain character between them
def join(input, glue = ' '.freeze) def join(input, glue = ' ')
InputIterator.new(input).join(glue) InputIterator.new(input).join(glue)
end end
@@ -120,19 +137,18 @@ module Liquid
# provide optional property with which to sort an array of hashes or drops # provide optional property with which to sort an array of hashes or drops
def sort(input, property = nil) def sort(input, property = nil)
ary = InputIterator.new(input) ary = InputIterator.new(input)
return [] if ary.empty?
if property.nil? if property.nil?
ary.sort
elsif ary.empty? # The next two cases assume a non-empty array.
[]
elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
ary.sort do |a, b| ary.sort do |a, b|
a = a[property] nil_safe_compare(a, b)
b = b[property] end
if a && b elsif ary.all? { |el| el.respond_to?(:[]) }
a <=> b begin
else ary.sort { |a, b| nil_safe_compare(a[property], b[property]) }
a ? -1 : 1 rescue TypeError
end raise_property_error(property)
end end
end end
end end
@@ -142,12 +158,40 @@ module Liquid
def sort_natural(input, property = nil) def sort_natural(input, property = nil)
ary = InputIterator.new(input) ary = InputIterator.new(input)
return [] if ary.empty?
if property.nil? if property.nil?
ary.sort { |a, b| a.casecmp(b) } ary.sort do |a, b|
elsif ary.empty? # The next two cases assume a non-empty array. nil_safe_casecmp(a, b)
end
elsif ary.all? { |el| el.respond_to?(:[]) }
begin
ary.sort { |a, b| nil_safe_casecmp(a[property], b[property]) }
rescue TypeError
raise_property_error(property)
end
end
end
# Filter the elements of an array to those with a certain property value.
# By default the target is any truthy value.
def where(input, property, target_value = nil)
ary = InputIterator.new(input)
if ary.empty?
[] []
elsif ary.first.respond_to?(:[]) && !ary.first[property].nil? elsif ary.first.respond_to?(:[]) && target_value.nil?
ary.sort { |a, b| a[property].casecmp(b[property]) } begin
ary.select { |item| item[property] }
rescue TypeError
raise_property_error(property)
end
elsif ary.first.respond_to?(:[])
begin
ary.select { |item| item[property] == target_value }
rescue TypeError
raise_property_error(property)
end
end end
end end
@@ -161,7 +205,11 @@ module Liquid
elsif ary.empty? # The next two cases assume a non-empty array. elsif ary.empty? # The next two cases assume a non-empty array.
[] []
elsif ary.first.respond_to?(:[]) elsif ary.first.respond_to?(:[])
ary.uniq{ |a| a[property] } begin
ary.uniq { |a| a[property] }
rescue TypeError
raise_property_error(property)
end
end end
end end
@@ -176,13 +224,15 @@ module Liquid
InputIterator.new(input).map do |e| InputIterator.new(input).map do |e|
e = e.call if e.is_a?(Proc) e = e.call if e.is_a?(Proc)
if property == "to_liquid".freeze if property == 'to_liquid'
e e
elsif e.respond_to?(:[]) elsif e.respond_to?(:[])
r = e[property] r = e[property]
r.is_a?(Proc) ? r.call : r r.is_a?(Proc) ? r.call : r
end end
end end
rescue TypeError
raise_property_error(property)
end end
# Remove nils within an array # Remove nils within an array
@@ -195,28 +245,32 @@ module Liquid
elsif ary.empty? # The next two cases assume a non-empty array. elsif ary.empty? # The next two cases assume a non-empty array.
[] []
elsif ary.first.respond_to?(:[]) elsif ary.first.respond_to?(:[])
ary.reject{ |a| a[property].nil? } begin
ary.reject { |a| a[property].nil? }
rescue TypeError
raise_property_error(property)
end
end end
end end
# Replace occurrences of a string with another # Replace occurrences of a string with another
def replace(input, string, replacement = ''.freeze) def replace(input, string, replacement = '')
input.to_s.gsub(string.to_s, replacement.to_s) input.to_s.gsub(string.to_s, replacement.to_s)
end end
# Replace the first occurrences of a string with another # Replace the first occurrences of a string with another
def replace_first(input, string, replacement = ''.freeze) def replace_first(input, string, replacement = '')
input.to_s.sub(string.to_s, replacement.to_s) input.to_s.sub(string.to_s, replacement.to_s)
end end
# remove a substring # remove a substring
def remove(input, string) def remove(input, string)
input.to_s.gsub(string.to_s, ''.freeze) input.to_s.gsub(string.to_s, '')
end end
# remove the first occurrences of a substring # remove the first occurrences of a substring
def remove_first(input, string) def remove_first(input, string)
input.to_s.sub(string.to_s, ''.freeze) input.to_s.sub(string.to_s, '')
end end
# add one string to another # add one string to another
@@ -225,9 +279,8 @@ module Liquid
end end
def concat(input, array) def concat(input, array)
unless array.respond_to?(:to_ary) raise ArgumentError, 'concat filter requires an array argument' unless array.respond_to?(:to_ary)
raise ArgumentError.new("concat filter requires an array argument")
end
InputIterator.new(input).concat(array) InputIterator.new(input).concat(array)
end end
@@ -238,7 +291,7 @@ module Liquid
# Add <br /> tags in front of all newlines in input string # Add <br /> tags in front of all newlines in input string
def newline_to_br(input) def newline_to_br(input)
input.to_s.gsub(/\n/, "<br />\n".freeze) input.to_s.gsub(/\n/, "<br />\n")
end end
# Reformat a date using Ruby's core Time#strftime( string ) -> string # Reformat a date using Ruby's core Time#strftime( string ) -> string
@@ -275,7 +328,7 @@ module Liquid
def date(input, format) def date(input, format)
return input if format.to_s.empty? return input if format.to_s.empty?
return input unless date = Utils.to_date(input) return input unless (date = Utils.to_date(input))
date.strftime(format.to_s) date.strftime(format.to_s)
end end
@@ -353,7 +406,23 @@ module Liquid
raise Liquid::FloatDomainError, e.message raise Liquid::FloatDomainError, e.message
end end
def default(input, default_value = ''.freeze) def at_least(input, n)
min_value = Utils.to_number(n)
result = Utils.to_number(input)
result = min_value if min_value > result
result.is_a?(BigDecimal) ? result.to_f : result
end
def at_most(input, n)
max_value = Utils.to_number(n)
result = Utils.to_number(input)
result = max_value if max_value < result
result.is_a?(BigDecimal) ? result.to_f : result
end
def default(input, default_value = '')
if !input || input.respond_to?(:empty?) && input.empty? if !input || input.respond_to?(:empty?) && input.empty?
default_value default_value
else else
@@ -363,11 +432,31 @@ module Liquid
private private
def raise_property_error(property)
raise Liquid::ArgumentError, "cannot select the property '#{property}'"
end
def apply_operation(input, operand, operation) def apply_operation(input, operand, operation)
result = Utils.to_number(input).send(operation, Utils.to_number(operand)) result = Utils.to_number(input).send(operation, Utils.to_number(operand))
result.is_a?(BigDecimal) ? result.to_f : result result.is_a?(BigDecimal) ? result.to_f : result
end end
def nil_safe_compare(a, b)
if !a.nil? && !b.nil?
a <=> b
else
a.nil? ? 1 : -1
end
end
def nil_safe_casecmp(a, b)
if !a.nil? && !b.nil?
a.to_s.casecmp(b.to_s)
else
a.nil? ? 1 : -1
end
end
class InputIterator class InputIterator
include Enumerable include Enumerable

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'set' require 'set'
module Liquid module Liquid
@@ -7,12 +9,12 @@ module Liquid
# The Strainer only allows method calls defined in filters given to it via Strainer.global_filter, # The Strainer only allows method calls defined in filters given to it via Strainer.global_filter,
# Context#add_filters or Template.register_filter # Context#add_filters or Template.register_filter
class Strainer #:nodoc: class Strainer #:nodoc:
@@global_strainer = Class.new(Strainer) do @global_strainer = Class.new(Strainer) do
@filter_methods = Set.new @filter_methods = Set.new
end end
@@strainer_class_cache = Hash.new do |hash, filters| @strainer_class_cache = Hash.new do |hash, filters|
hash[filters] = Class.new(@@global_strainer) do hash[filters] = Class.new(@global_strainer) do
@filter_methods = @@global_strainer.filter_methods.dup @filter_methods = Strainer.global_strainer.filter_methods.dup
filters.each { |f| add_filter(f) } filters.each { |f| add_filter(f) }
end end
end end
@@ -22,12 +24,14 @@ module Liquid
end end
class << self class << self
attr_accessor :strainer_class_cache, :global_strainer
attr_reader :filter_methods attr_reader :filter_methods
end end
def self.add_filter(filter) def self.add_filter(filter)
raise ArgumentError, "Expected module but got: #{filter.class}" unless filter.is_a?(Module) raise ArgumentError, "Expected module but got: #{filter.class}" unless filter.is_a?(Module)
unless self.include?(filter)
unless include?(filter)
invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) } invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }
if invokable_non_public_methods.any? if invokable_non_public_methods.any?
raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}" raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"
@@ -39,8 +43,8 @@ module Liquid
end end
def self.global_filter(filter) def self.global_filter(filter)
@@strainer_class_cache.clear @strainer_class_cache.clear
@@global_strainer.add_filter(filter) @global_strainer.add_filter(filter)
end end
def self.invokable?(method) def self.invokable?(method)
@@ -48,13 +52,13 @@ module Liquid
end end
def self.create(context, filters = []) def self.create(context, filters = [])
@@strainer_class_cache[filters].new(context) @strainer_class_cache[filters].new(context)
end end
def invoke(method, *args) def invoke(method, *args)
if self.class.invokable?(method) if self.class.invokable?(method)
send(method, *args) send(method, *args)
elsif @context && @context.strict_filters elsif @context&.strict_filters
raise Liquid::UndefinedFilter, "undefined filter #{method}" raise Liquid::UndefinedFilter, "undefined filter #{method}"
else else
args.first args.first

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
class TablerowloopDrop < Drop class TablerowloopDrop < Drop
def initialize(length, cols) def initialize(length, cols)

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
class Tag class Tag
attr_reader :nodelist, :tag_name, :line_number, :parse_context attr_reader :nodelist, :tag_name, :line_number, :parse_context
@@ -21,8 +23,7 @@ module Liquid
@line_number = parse_context.line_number @line_number = parse_context.line_number
end end
def parse(_tokens) def parse(_tokens); end
end
def raw def raw
"#{@tag_name} #{@markup}" "#{@tag_name} #{@markup}"
@@ -33,7 +34,15 @@ module Liquid
end end
def render(_context) def render(_context)
''.freeze ''
end
# For backwards compatibility with custom tags. In a future release, the semantics
# of the `render_to_output_buffer` method will become the default and the `render`
# method will be removed.
def render_to_output_buffer(context, output)
output << render(context)
output
end end
def blank? def blank?

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
# Assign sets a variable in your template. # Assign sets a variable in your template.
# #
@@ -8,23 +10,25 @@ module Liquid
# {{ foo }} # {{ foo }}
# #
class Assign < Tag class Assign < Tag
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om SYNTAX = /(#{VARIABLE_SIGNATURE}+)\s*=\s*(.*)\s*/om.freeze
attr_reader :to, :from
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
if markup =~ Syntax if markup =~ SYNTAX
@to = $1 @to = Regexp.last_match(1)
@from = Variable.new($2, options) @from = Variable.new(Regexp.last_match(2), options)
else else
raise SyntaxError.new options[:locale].t("errors.syntax.assign".freeze) raise SyntaxError, options[:locale].t('errors.syntax.assign')
end end
end end
def render(context) def render_to_output_buffer(context, output)
val = @from.render(context) val = @from.render(context)
context.scopes.last[@to] = val context.scopes.last[@to] = val
context.resource_limits.assign_score += assign_score_of(val) context.resource_limits.assign_score += assign_score_of(val)
''.freeze output
end end
def blank? def blank?
@@ -35,7 +39,7 @@ module Liquid
def assign_score_of(val) def assign_score_of(val)
if val.instance_of?(String) if val.instance_of?(String)
val.length val.bytesize
elsif val.instance_of?(Array) || val.instance_of?(Hash) elsif val.instance_of?(Array) || val.instance_of?(Hash)
sum = 1 sum = 1
# Uses #each to avoid extra allocations. # Uses #each to avoid extra allocations.
@@ -45,7 +49,13 @@ module Liquid
1 1
end end
end end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[@node.from]
end
end
end end
Template.register_tag('assign'.freeze, Assign) Template.register_tag('assign', Assign)
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
# Break tag to be used to break out of a for loop. # Break tag to be used to break out of a for loop.
# #
@@ -14,5 +16,5 @@ module Liquid
end end
end end
Template.register_tag('break'.freeze, Break) Template.register_tag('break', Break)
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
# Capture stores the result of a block into a variable without rendering it inplace. # Capture stores the result of a block into a variable without rendering it inplace.
# #
@@ -11,22 +13,23 @@ module Liquid
# in a sidebar or footer. # in a sidebar or footer.
# #
class Capture < Block class Capture < Block
Syntax = /(#{VariableSignature}+)/o SYNTAX = /(#{VARIABLE_SIGNATURE}+)/o.freeze
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
if markup =~ Syntax if markup =~ SYNTAX
@to = $1 @to = Regexp.last_match(1)
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.capture")) raise SyntaxError, options[:locale].t('errors.syntax.capture')
end end
end end
def render(context) def render_to_output_buffer(context, output)
output = super previous_output_size = output.bytesize
super
context.scopes.last[@to] = output context.scopes.last[@to] = output
context.resource_limits.assign_score += output.length context.resource_limits.assign_score += (output.bytesize - previous_output_size)
''.freeze output
end end
def blank? def blank?
@@ -34,5 +37,5 @@ module Liquid
end end
end end
Template.register_tag('capture'.freeze, Capture) Template.register_tag('capture', Capture)
end end

View File

@@ -1,24 +1,26 @@
# frozen_string_literal: true
module Liquid module Liquid
class Case < Block class Case < Block
Syntax = /(#{QuotedFragment})/o SYNTAX = /(#{QUOTED_FRAGMENT})/o.freeze
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om WHEN_SYNTAX = /(#{QUOTED_FRAGMENT})(?:(?:\s+or\s+|\s*\,\s*)(#{QUOTED_FRAGMENT}.*))?/om.freeze
attr_reader :blocks, :left
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
@blocks = [] @blocks = []
if markup =~ Syntax if markup =~ SYNTAX
@left = Expression.parse($1) @left = Expression.parse(Regexp.last_match(1))
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.case".freeze)) raise SyntaxError, options[:locale].t('errors.syntax.case')
end end
end end
def parse(tokens) def parse(tokens)
body = BlockBody.new body = BlockBody.new
while parse_body(body, tokens) body = @blocks.last.attachment while parse_body(body, tokens)
body = @blocks.last.attachment
end
end end
def nodelist def nodelist
@@ -27,30 +29,30 @@ module Liquid
def unknown_tag(tag, markup, tokens) def unknown_tag(tag, markup, tokens)
case tag case tag
when 'when'.freeze when 'when'
record_when_condition(markup) record_when_condition(markup)
when 'else'.freeze when 'else'
record_else_condition(markup) record_else_condition(markup)
else else
super super
end end
end end
def render(context) def render_to_output_buffer(context, output)
context.stack do context.stack do
execute_else_block = true execute_else_block = true
output = ''
@blocks.each do |block| @blocks.each do |block|
if block.else? if block.else?
return block.attachment.render(context) if execute_else_block block.attachment.render_to_output_buffer(context, output) if execute_else_block
elsif block.evaluate(context) elsif block.evaluate(context)
execute_else_block = false execute_else_block = false
output << block.attachment.render(context) block.attachment.render_to_output_buffer(context, output)
end end
end end
output
end end
output
end end
private private
@@ -59,28 +61,30 @@ module Liquid
body = BlockBody.new body = BlockBody.new
while markup while markup
unless markup =~ WhenSyntax raise SyntaxError, options[:locale].t('errors.syntax.case_invalid_when') unless markup =~ WHEN_SYNTAX
raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_when".freeze))
end
markup = $2 markup = Regexp.last_match(2)
block = Condition.new(@left, '=='.freeze, Expression.parse($1)) block = Condition.new(@left, '==', Expression.parse(Regexp.last_match(1)))
block.attach(body) block.attach(body)
@blocks << block @blocks << block
end end
end end
def record_else_condition(markup) def record_else_condition(markup)
unless markup.strip.empty? raise SyntaxError, options[:locale].t('errors.syntax.case_invalid_else') unless markup.strip.empty?
raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_else".freeze))
end
block = ElseCondition.new block = ElseCondition.new
block.attach(BlockBody.new) block.attach(BlockBody.new)
@blocks << block @blocks << block
end end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[@node.left] + @node.blocks
end
end
end end
Template.register_tag('case'.freeze, Case) Template.register_tag('case', Case)
end end

View File

@@ -1,16 +1,17 @@
# frozen_string_literal: true
module Liquid module Liquid
class Comment < Block class Comment < Block
def render(_context) def render_to_output_buffer(_context, output)
''.freeze output
end end
def unknown_tag(_tag, _markup, _tokens) def unknown_tag(_tag, _markup, _tokens); end
end
def blank? def blank?
true true
end end
end end
Template.register_tag('comment'.freeze, Comment) Template.register_tag('comment', Comment)
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
# Continue tag to be used to break out of a for loop. # Continue tag to be used to break out of a for loop.
# #
@@ -14,5 +16,5 @@ module Liquid
end end
end end
Template.register_tag('continue'.freeze, Continue) Template.register_tag('continue', Continue)
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
# Cycle is usually used within a loop to alternate between values, like colors or DOM classes. # Cycle is usually used within a loop to alternate between values, like colors or DOM classes.
# #
@@ -12,45 +14,64 @@ module Liquid
# <div class="green"> Item five</div> # <div class="green"> Item five</div>
# #
class Cycle < Tag class Cycle < Tag
SimpleSyntax = /\A#{QuotedFragment}+/o SIMPLE_SYNTAX = /\A#{QUOTED_FRAGMENT}+/o.freeze
NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om NAMED_SYNTAX = /\A(#{QUOTED_FRAGMENT})\s*\:\s*(.*)/om.freeze
attr_reader :variables
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
case markup case markup
when NamedSyntax when NAMED_SYNTAX
@variables = variables_from_string($2) @variables = variables_from_string(Regexp.last_match(2))
@name = Expression.parse($1) @name = Expression.parse(Regexp.last_match(1))
when SimpleSyntax when SIMPLE_SYNTAX
@variables = variables_from_string(markup) @variables = variables_from_string(markup)
@name = @variables.to_s @name = @variables.to_s
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.cycle".freeze)) raise SyntaxError, options[:locale].t('errors.syntax.cycle')
end end
end end
def render(context) def render_to_output_buffer(context, output)
context.registers[:cycle] ||= Hash.new(0) context.registers[:cycle] ||= {}
context.stack do context.stack do
key = context.evaluate(@name) key = context.evaluate(@name)
iteration = context.registers[:cycle][key] iteration = context.registers[:cycle][key].to_i
result = context.evaluate(@variables[iteration])
val = context.evaluate(@variables[iteration])
if val.is_a?(Array)
val = val.join
elsif !val.is_a?(String)
val = val.to_s
end
output << val
iteration += 1 iteration += 1
iteration = 0 if iteration >= @variables.size iteration = 0 if iteration >= @variables.size
context.registers[:cycle][key] = iteration context.registers[:cycle][key] = iteration
result
end end
output
end end
private private
def variables_from_string(markup) def variables_from_string(markup)
markup.split(',').collect do |var| markup.split(',').collect do |var|
var =~ /\s*(#{QuotedFragment})\s*/o var =~ /\s*(#{QUOTED_FRAGMENT})\s*/o
$1 ? Expression.parse($1) : nil Regexp.last_match(1) ? Expression.parse(Regexp.last_match(1)) : nil
end.compact end.compact
end end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
Array(@node.variables)
end
end
end end
Template.register_tag('cycle', Cycle) Template.register_tag('cycle', Cycle)

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
# decrement is used in a place where one needs to insert a counter # decrement is used in a place where one needs to insert a counter
# into a template, and needs the counter to survive across # into a template, and needs the counter to survive across
@@ -23,13 +25,14 @@ module Liquid
@variable = markup.strip @variable = markup.strip
end end
def render(context) def render_to_output_buffer(context, output)
value = context.environments.first[@variable] ||= 0 value = context.environments.first[@variable] ||= 0
value -= 1 value -= 1
context.environments.first[@variable] = value context.environments.first[@variable] = value
value.to_s output << value.to_s
output
end end
end end
Template.register_tag('decrement'.freeze, Decrement) Template.register_tag('decrement', Decrement)
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
# "For" iterates over an array or collection. # "For" iterates over an array or collection.
# Several useful variables are available to you within the loop. # Several useful variables are available to you within the loop.
@@ -44,10 +46,9 @@ module Liquid
# forloop.parentloop:: Provides access to the parent loop, if present. # forloop.parentloop:: Provides access to the parent loop, if present.
# #
class For < Block class For < Block
Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o SYNTAX = /\A(#{VARIABLE_SEGMENT}+)\s+in\s+(#{QUOTED_FRAGMENT}+)\s*(reversed)?/o.freeze
attr_reader :collection_name attr_reader :collection_name, :variable_name, :limit, :from
attr_reader :variable_name
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
@@ -59,6 +60,7 @@ module Liquid
def parse(tokens) def parse(tokens)
return unless parse_body(@for_block, tokens) return unless parse_body(@for_block, tokens)
parse_body(@else_block, tokens) parse_body(@else_block, tokens)
end end
@@ -67,50 +69,55 @@ module Liquid
end end
def unknown_tag(tag, markup, tokens) def unknown_tag(tag, markup, tokens)
return super unless tag == 'else'.freeze return super unless tag == 'else'
@else_block = BlockBody.new @else_block = BlockBody.new
end end
def render(context) def render_to_output_buffer(context, output)
segment = collection_segment(context) segment = collection_segment(context)
if segment.empty? if segment.empty?
render_else(context) render_else(context, output)
else else
render_segment(context, segment) render_segment(context, output, segment)
end end
output
end end
protected protected
def lax_parse(markup) def lax_parse(markup)
if markup =~ Syntax if markup =~ SYNTAX
@variable_name = $1 @variable_name = Regexp.last_match(1)
collection_name = $2 collection_name = Regexp.last_match(2)
@reversed = !!$3 @reversed = !!Regexp.last_match(3)
@name = "#{@variable_name}-#{collection_name}" @name = "#{@variable_name}-#{collection_name}"
@collection_name = Expression.parse(collection_name) @collection_name = Expression.parse(collection_name)
markup.scan(TagAttributes) do |key, value| markup.scan(TAG_ATTRIBUTES) do |key, value|
set_attribute(key, value) set_attribute(key, value)
end end
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.for".freeze)) raise SyntaxError, options[:locale].t('errors.syntax.for')
end end
end end
def strict_parse(markup) def strict_parse(markup)
p = Parser.new(markup) p = Parser.new(markup)
@variable_name = p.consume(:id) @variable_name = p.consume(:id)
raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_in".freeze)) unless p.id?('in'.freeze) raise SyntaxError, options[:locale].t('errors.syntax.for_invalid_in') unless p.id?('in')
collection_name = p.expression collection_name = p.expression
@name = "#{@variable_name}-#{collection_name}" @name = "#{@variable_name}-#{collection_name}"
@collection_name = Expression.parse(collection_name) @collection_name = Expression.parse(collection_name)
@reversed = p.id?('reversed'.freeze) @reversed = p.id?('reversed')
while p.look(:id) && p.look(:colon, 1) while p.look(:id) && p.look(:colon, 1)
unless attribute = p.id?('limit'.freeze) || p.id?('offset'.freeze) unless (attribute = p.id?('limit') || p.id?('offset'))
raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_attribute".freeze)) raise SyntaxError, options[:locale].t('errors.syntax.for_invalid_attribute')
end end
p.consume p.consume
set_attribute(attribute, p.expression) set_attribute(attribute, p.expression)
end end
@@ -120,19 +127,28 @@ module Liquid
private private
def collection_segment(context) def collection_segment(context)
offsets = context.registers[:for] ||= Hash.new(0) offsets = context.registers[:for] ||= {}
from = if @from == :continue from = if @from == :continue
offsets[@name].to_i offsets[@name].to_i
else else
context.evaluate(@from).to_i from_value = context.evaluate(@from)
if from_value.nil?
0
else
Utils.to_integer(from_value)
end
end end
collection = context.evaluate(@collection_name) collection = context.evaluate(@collection_name)
collection = collection.to_a if collection.is_a?(Range) collection = collection.to_a if collection.is_a?(Range)
limit = context.evaluate(@limit) limit_value = context.evaluate(@limit)
to = limit ? limit.to_i + from : nil to = if limit_value.nil?
nil
else
Utils.to_integer(limit_value) + from
end
segment = Utils.slice_collection(collection, from, to) segment = Utils.slice_collection(collection, from, to)
segment.reverse! if @reversed segment.reverse! if @reversed
@@ -142,57 +158,65 @@ module Liquid
segment segment
end end
def render_segment(context, segment) def render_segment(context, output, segment)
for_stack = context.registers[:for_stack] ||= [] for_stack = context.registers[:for_stack] ||= []
length = segment.length length = segment.length
result = ''
context.stack do context.stack do
loop_vars = Liquid::ForloopDrop.new(@name, length, for_stack[-1]) loop_vars = Liquid::ForloopDrop.new(@name, length, for_stack[-1])
for_stack.push(loop_vars) for_stack.push(loop_vars)
begin begin
context['forloop'.freeze] = loop_vars context['forloop'] = loop_vars
segment.each_with_index do |item, index| segment.each do |item|
context[@variable_name] = item context[@variable_name] = item
result << @for_block.render(context) @for_block.render_to_output_buffer(context, output)
loop_vars.send(:increment!) loop_vars.send(:increment!)
# Handle any interrupts if they exist. # Handle any interrupts if they exist.
if context.interrupt? next unless context.interrupt?
interrupt = context.pop_interrupt
break if interrupt.is_a? BreakInterrupt interrupt = context.pop_interrupt
next if interrupt.is_a? ContinueInterrupt break if interrupt.is_a?(BreakInterrupt)
end next if interrupt.is_a?(ContinueInterrupt)
end end
ensure ensure
for_stack.pop for_stack.pop
end end
end end
result output
end end
def set_attribute(key, expr) def set_attribute(key, expr)
case key case key
when 'offset'.freeze when 'offset'
@from = if expr == 'continue'.freeze @from = if expr == 'continue'
:continue :continue
else else
Expression.parse(expr) Expression.parse(expr)
end end
when 'limit'.freeze when 'limit'
@limit = Expression.parse(expr) @limit = Expression.parse(expr)
end end
end end
def render_else(context) def render_else(context, output)
@else_block ? @else_block.render(context) : ''.freeze if @else_block
@else_block.render_to_output_buffer(context, output)
else
output
end
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
(super + [@node.limit, @node.from, @node.collection_name]).compact
end
end end
end end
Template.register_tag('for'.freeze, For) Template.register_tag('for', For)
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
# If is the conditional block # If is the conditional block
# #
@@ -10,14 +12,20 @@ module Liquid
# There are {% if count < 5 %} less {% else %} more {% endif %} items than you need. # There are {% if count < 5 %} less {% else %} more {% endif %} items than you need.
# #
class If < Block class If < Block
Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o SYNTAX = /(#{QUOTED_FRAGMENT})\s*([=!<>a-z_]+)?\s*(#{QUOTED_FRAGMENT})?/o.freeze
ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o EXPRESSIONS_AND_OPERATORS = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QUOTED_FRAGMENT}|\S+)\s*)+)/o.freeze
BOOLEAN_OPERATORS = %w(and or) BOOLEAN_OPERATORS = %w[and or].freeze
attr_reader :blocks
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
@blocks = [] @blocks = []
push_block('if'.freeze, markup) push_block('if', markup)
end
def nodelist
@blocks.map(&:attachment)
end end
def parse(tokens) def parse(tokens)
@@ -25,33 +33,28 @@ module Liquid
end end
end end
def nodelist
@blocks.map(&:attachment)
end
def unknown_tag(tag, markup, tokens) def unknown_tag(tag, markup, tokens)
if ['elsif'.freeze, 'else'.freeze].include?(tag) if %w[elsif else].include?(tag)
push_block(tag, markup) push_block(tag, markup)
else else
super super
end end
end end
def render(context) def render_to_output_buffer(context, output)
context.stack do context.stack do
@blocks.each do |block| @blocks.each do |block|
if block.evaluate(context) return block.attachment.render_to_output_buffer(context, output) if block.evaluate(context)
return block.attachment.render(context)
end
end end
''.freeze
end end
output
end end
private private
def push_block(tag, markup) def push_block(tag, markup)
block = if tag == 'else'.freeze block = if tag == 'else'
ElseCondition.new ElseCondition.new
else else
parse_with_selected_parser(markup) parse_with_selected_parser(markup)
@@ -62,18 +65,19 @@ module Liquid
end end
def lax_parse(markup) def lax_parse(markup)
expressions = markup.scan(ExpressionsAndOperators) expressions = markup.scan(EXPRESSIONS_AND_OPERATORS)
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop =~ Syntax raise SyntaxError, options[:locale].t('errors.syntax.if') unless expressions.pop =~ SYNTAX
condition = Condition.new(Expression.parse($1), $2, Expression.parse($3)) condition = Condition.new(Expression.parse(Regexp.last_match(1)), Regexp.last_match(2), Expression.parse(Regexp.last_match(3)))
until expressions.empty? until expressions.empty?
operator = expressions.pop.to_s.strip operator = expressions.pop.to_s.strip
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop.to_s =~ Syntax raise SyntaxError, options[:locale].t('errors.syntax.if') unless expressions.pop.to_s =~ SYNTAX
new_condition = Condition.new(Expression.parse(Regexp.last_match(1)), Regexp.last_match(2), Expression.parse(Regexp.last_match(3)))
raise SyntaxError, options[:locale].t('errors.syntax.if') unless BOOLEAN_OPERATORS.include?(operator)
new_condition = Condition.new(Expression.parse($1), $2, Expression.parse($3))
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless BOOLEAN_OPERATORS.include?(operator)
new_condition.send(operator, condition) new_condition.send(operator, condition)
condition = new_condition condition = new_condition
end end
@@ -91,7 +95,7 @@ module Liquid
def parse_binary_comparisons(p) def parse_binary_comparisons(p)
condition = parse_comparison(p) condition = parse_comparison(p)
first_condition = condition first_condition = condition
while op = (p.id?('and'.freeze) || p.id?('or'.freeze)) while (op = (p.id?('and') || p.id?('or')))
child_condition = parse_comparison(p) child_condition = parse_comparison(p)
condition.send(op, child_condition) condition.send(op, child_condition)
condition = child_condition condition = child_condition
@@ -101,14 +105,20 @@ module Liquid
def parse_comparison(p) def parse_comparison(p)
a = Expression.parse(p.expression) a = Expression.parse(p.expression)
if op = p.consume?(:comparison) if (op = p.consume?(:comparison))
b = Expression.parse(p.expression) b = Expression.parse(p.expression)
Condition.new(a, op, b) Condition.new(a, op, b)
else else
Condition.new(a) Condition.new(a)
end end
end end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
@node.blocks
end
end
end end
Template.register_tag('if'.freeze, If) Template.register_tag('if', If)
end end

View File

@@ -1,18 +1,21 @@
# frozen_string_literal: true
module Liquid module Liquid
class Ifchanged < Block class Ifchanged < Block
def render(context) def render_to_output_buffer(context, output)
context.stack do context.stack do
output = super block_output = +''
super(context, block_output)
if output != context.registers[:ifchanged] if block_output != context.registers[:ifchanged]
context.registers[:ifchanged] = output context.registers[:ifchanged] = block_output
output output << block_output
else
''.freeze
end end
end end
output
end end
end end
Template.register_tag('ifchanged'.freeze, Ifchanged) Template.register_tag('ifchanged', Ifchanged)
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
# Include allows templates to relate with other templates # Include allows templates to relate with other templates
# #
@@ -14,38 +16,39 @@ module Liquid
# {% include 'product' for products %} # {% include 'product' for products %}
# #
class Include < Tag class Include < Tag
Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/o SYNTAX = /(#{QUOTED_FRAGMENT}+)(\s+(?:with|for)\s+(#{QUOTED_FRAGMENT}+))?/o.freeze
attr_reader :template_name_expr, :variable_name_expr, :attributes
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
if markup =~ Syntax if markup =~ SYNTAX
template_name = $1 template_name = Regexp.last_match(1)
variable_name = $3 variable_name = Regexp.last_match(3)
@variable_name_expr = variable_name ? Expression.parse(variable_name) : nil @variable_name_expr = variable_name ? Expression.parse(variable_name) : nil
@template_name_expr = Expression.parse(template_name) @template_name_expr = Expression.parse(template_name)
@attributes = {} @attributes = {}
markup.scan(TagAttributes) do |key, value| markup.scan(TAG_ATTRIBUTES) do |key, value|
@attributes[key] = Expression.parse(value) @attributes[key] = Expression.parse(value)
end end
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.include".freeze)) raise SyntaxError, options[:locale].t('errors.syntax.include')
end end
end end
def parse(_tokens) def parse(_tokens); end
end
def render(context) def render_to_output_buffer(context, output)
template_name = context.evaluate(@template_name_expr) template_name = context.evaluate(@template_name_expr)
raise ArgumentError.new(options[:locale].t("errors.argument.include")) unless template_name raise ArgumentError, options[:locale].t('errors.argument.include') unless template_name
partial = load_cached_partial(template_name, context) partial = load_cached_partial(template_name, context)
context_variable_name = template_name.split('/'.freeze).last context_variable_name = template_name.split('/').last
variable = if @variable_name_expr variable = if @variable_name_expr
context.evaluate(@variable_name_expr) context.evaluate(@variable_name_expr)
@@ -64,19 +67,21 @@ module Liquid
end end
if variable.is_a?(Array) if variable.is_a?(Array)
variable.collect do |var| variable.each do |var|
context[context_variable_name] = var context[context_variable_name] = var
partial.render(context) partial.render_to_output_buffer(context, output)
end end
else else
context[context_variable_name] = variable context[context_variable_name] = variable
partial.render(context) partial.render_to_output_buffer(context, output)
end end
end end
ensure ensure
context.template_name = old_template_name context.template_name = old_template_name
context.partial = old_partial context.partial = old_partial
end end
output
end end
private private
@@ -87,9 +92,10 @@ module Liquid
def load_cached_partial(template_name, context) def load_cached_partial(template_name, context)
cached_partials = context.registers[:cached_partials] || {} cached_partials = context.registers[:cached_partials] || {}
if cached = cached_partials[template_name] if (cached = cached_partials[template_name])
return cached return cached
end end
source = read_template_from_file_system(context) source = read_template_from_file_system(context)
begin begin
parse_context.partial = true parse_context.partial = true
@@ -107,7 +113,16 @@ module Liquid
file_system.read_template_file(context.evaluate(@template_name_expr)) file_system.read_template_file(context.evaluate(@template_name_expr))
end end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[
@node.template_name_expr,
@node.variable_name_expr,
] + @node.attributes.values
end
end
end end
Template.register_tag('include'.freeze, Include) Template.register_tag('include', Include)
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
# increment is used in a place where one needs to insert a counter # increment is used in a place where one needs to insert a counter
# into a template, and needs the counter to survive across # into a template, and needs the counter to survive across
@@ -20,12 +22,13 @@ module Liquid
@variable = markup.strip @variable = markup.strip
end end
def render(context) def render_to_output_buffer(context, output)
value = context.environments.first[@variable] ||= 0 value = context.environments.first[@variable] ||= 0
context.environments.first[@variable] = value + 1 context.environments.first[@variable] = value + 1
value.to_s output << value.to_s
output
end end
end end
Template.register_tag('increment'.freeze, Increment) Template.register_tag('increment', Increment)
end end

View File

@@ -1,7 +1,9 @@
# frozen_string_literal: true
module Liquid module Liquid
class Raw < Block class Raw < Block
Syntax = /\A\s*\z/ SYNTAX = /\A\s*\z/.freeze
FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om FULL_TOKEN_POSSIBLY_INVALID = /\A(.*)#{TAG_START}\s*(\w+)\s*(.*)?#{TAG_END}\z/om.freeze
def initialize(tag_name, markup, parse_context) def initialize(tag_name, markup, parse_context)
super super
@@ -10,20 +12,21 @@ module Liquid
end end
def parse(tokens) def parse(tokens)
@body = '' @body = +''
while token = tokens.shift while (token = tokens.shift)
if token =~ FullTokenPossiblyInvalid if token =~ FULL_TOKEN_POSSIBLY_INVALID
@body << $1 if $1 != "".freeze @body << Regexp.last_match(1) if Regexp.last_match(1) != ''
return if block_delimiter == $2 return if block_delimiter == Regexp.last_match(2)
end end
@body << token unless token.empty? @body << token unless token.empty?
end end
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name)) raise SyntaxError, parse_context.locale.t('errors.syntax.tag_never_closed', block_name: block_name)
end end
def render(_context) def render_to_output_buffer(_context, output)
@body output << @body
output
end end
def nodelist def nodelist
@@ -37,11 +40,9 @@ module Liquid
protected protected
def ensure_valid_markup(tag_name, markup, parse_context) def ensure_valid_markup(tag_name, markup, parse_context)
unless markup =~ Syntax raise SyntaxError, parse_context.locale.t('errors.syntax.tag_unexpected_args', tag: tag_name) unless markup =~ SYNTAX
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_unexpected_args".freeze, tag: tag_name))
end
end end
end end
Template.register_tag('raw'.freeze, Raw) Template.register_tag('raw', Raw)
end end

View File

@@ -1,54 +1,65 @@
# frozen_string_literal: true
module Liquid module Liquid
class TableRow < Block class TableRow < Block
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o SYNTAX = /(\w+)\s+in\s+(#{QUOTED_FRAGMENT}+)/o.freeze
attr_reader :variable_name, :collection_name, :attributes
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
if markup =~ Syntax if markup =~ SYNTAX
@variable_name = $1 @variable_name = Regexp.last_match(1)
@collection_name = Expression.parse($2) @collection_name = Expression.parse(Regexp.last_match(2))
@attributes = {} @attributes = {}
markup.scan(TagAttributes) do |key, value| markup.scan(TAG_ATTRIBUTES) do |key, value|
@attributes[key] = Expression.parse(value) @attributes[key] = Expression.parse(value)
end end
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.table_row".freeze)) raise SyntaxError, options[:locale].t('errors.syntax.table_row')
end end
end end
def render(context) def render_to_output_buffer(context, output)
collection = context.evaluate(@collection_name) or return ''.freeze (collection = context.evaluate(@collection_name)) || (return '')
from = @attributes.key?('offset'.freeze) ? context.evaluate(@attributes['offset'.freeze]).to_i : 0 from = @attributes.key?('offset') ? context.evaluate(@attributes['offset']).to_i : 0
to = @attributes.key?('limit'.freeze) ? from + context.evaluate(@attributes['limit'.freeze]).to_i : nil to = @attributes.key?('limit') ? from + context.evaluate(@attributes['limit']).to_i : nil
collection = Utils.slice_collection(collection, from, to) collection = Utils.slice_collection(collection, from, to)
length = collection.length length = collection.length
cols = context.evaluate(@attributes['cols'.freeze]).to_i cols = context.evaluate(@attributes['cols']).to_i
result = "<tr class=\"row1\">\n" output << "<tr class=\"row1\">\n"
context.stack do context.stack do
tablerowloop = Liquid::TablerowloopDrop.new(length, cols) tablerowloop = Liquid::TablerowloopDrop.new(length, cols)
context['tablerowloop'.freeze] = tablerowloop context['tablerowloop'] = tablerowloop
collection.each_with_index do |item, index| collection.each do |item|
context[@variable_name] = item context[@variable_name] = item
result << "<td class=\"col#{tablerowloop.col}\">" << super << '</td>' output << "<td class=\"col#{tablerowloop.col}\">"
super
output << '</td>'
if tablerowloop.col_last && !tablerowloop.last output << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">" if tablerowloop.col_last && !tablerowloop.last
result << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">"
end
tablerowloop.send(:increment!) tablerowloop.send(:increment!)
end end
end end
result << "</tr>\n"
result output << "</tr>\n"
output
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
super + @node.attributes.values + [@node.collection_name]
end
end end
end end
Template.register_tag('tablerow'.freeze, TableRow) Template.register_tag('tablerow', TableRow)
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require_relative 'if' require_relative 'if'
module Liquid module Liquid
@@ -6,25 +8,21 @@ module Liquid
# {% unless x < 0 %} x is greater than zero {% endunless %} # {% unless x < 0 %} x is greater than zero {% endunless %}
# #
class Unless < If class Unless < If
def render(context) def render_to_output_buffer(context, output)
context.stack do context.stack do
# First condition is interpreted backwards ( if not ) # First condition is interpreted backwards ( if not )
first_block = @blocks.first first_block = @blocks.first
unless first_block.evaluate(context) return first_block.attachment.render_to_output_buffer(context, output) unless first_block.evaluate(context)
return first_block.attachment.render(context)
end
# After the first condition unless works just like if # After the first condition unless works just like if
@blocks[1..-1].each do |block| @blocks[1..-1].each do |block|
if block.evaluate(context) return block.attachment.render_to_output_buffer(context, output) if block.evaluate(context)
return block.attachment.render(context)
end
end end
''.freeze
end end
output
end end
end end
Template.register_tag('unless'.freeze, Unless) Template.register_tag('unless', Unless)
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
# Templates are central to liquid. # Templates are central to liquid.
# Interpretating templates is a two step process. First you compile the # Interpretating templates is a two step process. First you compile the
@@ -16,7 +18,11 @@ module Liquid
attr_accessor :root attr_accessor :root
attr_reader :resource_limits, :warnings attr_reader :resource_limits, :warnings
@@file_system = BlankFileSystem.new @file_system = BlankFileSystem.new
class << self
attr_accessor :file_system
end
class TagRegistry class TagRegistry
include Enumerable include Enumerable
@@ -50,7 +56,7 @@ module Liquid
private private
def lookup_class(name) def lookup_class(name)
name.split("::").reject(&:empty?).reduce(Object) { |scope, const| scope.const_get(const) } Object.const_get(name)
end end
end end
@@ -74,14 +80,6 @@ module Liquid
exception exception
end end
def file_system
@@file_system
end
def file_system=(obj)
@@file_system = obj
end
def register_tag(name, klass) def register_tag(name, klass)
tags[name.to_s] = klass tags[name.to_s] = klass
end end
@@ -165,15 +163,13 @@ module Liquid
# filters and tags and might be useful to integrate liquid more with its host application # filters and tags and might be useful to integrate liquid more with its host application
# #
def render(*args) def render(*args)
return ''.freeze if @root.nil? return '' if @root.nil?
context = case args.first context = case args.first
when Liquid::Context when Liquid::Context
c = args.shift c = args.shift
if @rethrow_errors c.exception_renderer = ->(_e) { raise } if @rethrow_errors
c.exception_renderer = ->(e) { raise }
end
c c
when Liquid::Drop when Liquid::Drop
@@ -184,12 +180,15 @@ module Liquid
when nil when nil
Context.new(assigns, instance_assigns, registers, @rethrow_errors, @resource_limits) Context.new(assigns, instance_assigns, registers, @rethrow_errors, @resource_limits)
else else
raise ArgumentError, "Expected Hash or Liquid::Context as parameter" raise ArgumentError, 'Expected Hash or Liquid::Context as parameter'
end end
output = nil
case args.last case args.last
when Hash when Hash
options = args.pop options = args.pop
output = options[:output] if options[:output]
registers.merge!(options[:registers]) if options[:registers].is_a?(Hash) registers.merge!(options[:registers]) if options[:registers].is_a?(Hash)
@@ -204,10 +203,9 @@ module Liquid
begin begin
# render the nodelist. # render the nodelist.
# for performance reasons we get an array back here. join will make a string out of it. # for performance reasons we get an array back here. join will make a string out of it.
result = with_profiling(context) do with_profiling(context) do
@root.render(context) @root.render_to_output_buffer(context, output || +'')
end end
result.respond_to?(:join) ? result.join : result
rescue Liquid::MemoryError => e rescue Liquid::MemoryError => e
context.handle_error(e) context.handle_error(e)
ensure ensure
@@ -220,6 +218,10 @@ module Liquid
render(*args) render(*args)
end end
def render_to_output_buffer(context, output)
render(context, output: output)
end
private private
def tokenize(source) def tokenize(source)

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
class Tokenizer class Tokenizer
attr_reader :line_number attr_reader :line_number
@@ -20,10 +22,10 @@ module Liquid
@source = @source.source if @source.respond_to?(:source) @source = @source.source if @source.respond_to?(:source)
return [] if @source.to_s.empty? return [] if @source.to_s.empty?
tokens = @source.split(TemplateParser) tokens = @source.split(TEMPLATE_PARSER)
# removes the rogue empty element at the beginning of the array # removes the rogue empty element at the beginning of the array
tokens.shift if tokens[0] && tokens[0].empty? tokens.shift if tokens[0]&.empty?
tokens tokens
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
module Utils module Utils
def self.slice_collection(collection, from, to) def self.slice_collection(collection, from, to)
@@ -19,13 +21,9 @@ module Liquid
return [] unless collection.respond_to?(:each) return [] unless collection.respond_to?(:each)
collection.each do |item| collection.each do |item|
if to && to <= index break if to && to <= index
break
end
if from <= index segments << item if from <= index
segments << item
end
index += 1 index += 1
end end
@@ -35,22 +33,23 @@ module Liquid
def self.to_integer(num) def self.to_integer(num)
return num if num.is_a?(Integer) return num if num.is_a?(Integer)
num = num.to_s num = num.to_s
begin begin
Integer(num) Integer(num)
rescue ::ArgumentError rescue ::ArgumentError
raise Liquid::ArgumentError, "invalid integer" raise Liquid::ArgumentError, 'invalid integer'
end end
end end
def self.to_number(obj) def self.to_number(obj)
case obj case obj
when Float when Float
BigDecimal.new(obj.to_s) BigDecimal(obj.to_s)
when Numeric when Numeric
obj obj
when String when String
(obj.strip =~ /\A-?\d+\.\d+\z/) ? BigDecimal.new(obj) : obj.to_i obj.strip =~ /\A-?\d+\.\d+\z/ ? BigDecimal(obj) : obj.to_i
else else
if obj.respond_to?(:to_number) if obj.respond_to?(:to_number)
obj.to_number obj.to_number
@@ -65,11 +64,12 @@ module Liquid
if obj.is_a?(String) if obj.is_a?(String)
return nil if obj.empty? return nil if obj.empty?
obj = obj.downcase obj = obj.downcase
end end
case obj case obj
when 'now'.freeze, 'today'.freeze when 'now', 'today'
Time.now Time.now
when /\A\d+\z/, Integer when /\A\d+\z/, Integer
Time.at(obj.to_i) Time.at(obj.to_i)

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
# Holds variables. Variables are only loaded "just in time" # Holds variables. Variables are only loaded "just in time"
# and are not evaluated as part of the render stage # and are not evaluated as part of the render stage
@@ -10,10 +12,16 @@ module Liquid
# {{ user | link }} # {{ user | link }}
# #
class Variable class Variable
FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o FILTER_MARKUP_REGEX = /#{FILTER_SEPARATOR}\s*(.*)/om.freeze
FILTER_PARSER = /(?:\s+|#{QUOTED_FRAGMENT}|#{ARGUMENT_SEPARATOR})+/o.freeze
FILTER_ARGS_REGEX = /(?:#{FILTER_ARGUMENT_SEPARATOR}|#{ARGUMENT_SEPARATOR})\s*((?:\w+\s*\:\s*)?#{QUOTED_FRAGMENT})/o.freeze
JUST_TAG_ATTRIBUTES = /\A#{TAG_ATTRIBUTES}\z/o.freeze
MARKUP_WITH_QUOTED_FRAGMENT = /(#{QUOTED_FRAGMENT})(.*)/om.freeze
attr_accessor :filters, :name, :line_number attr_accessor :filters, :name, :line_number
attr_reader :parse_context attr_reader :parse_context
alias_method :options, :parse_context alias_method :options, :parse_context
include ParserSwitching include ParserSwitching
def initialize(markup, parse_context) def initialize(markup, parse_context)
@@ -35,17 +43,18 @@ module Liquid
def lax_parse(markup) def lax_parse(markup)
@filters = [] @filters = []
return unless markup =~ /(#{QuotedFragment})(.*)/om return unless markup =~ MARKUP_WITH_QUOTED_FRAGMENT
name_markup = $1 name_markup = Regexp.last_match(1)
filter_markup = $2 filter_markup = Regexp.last_match(2)
@name = Expression.parse(name_markup) @name = Expression.parse(name_markup)
if filter_markup =~ /#{FilterSeparator}\s*(.*)/om if filter_markup =~ FILTER_MARKUP_REGEX
filters = $1.scan(FilterParser) filters = Regexp.last_match(1).scan(FILTER_PARSER)
filters.each do |f| filters.each do |f|
next unless f =~ /\w+/ next unless f =~ /\w+/
filtername = Regexp.last_match(0) filtername = Regexp.last_match(0)
filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o).flatten filterargs = f.scan(FILTER_ARGS_REGEX).flatten
@filters << parse_filter_expressions(filtername, filterargs) @filters << parse_filter_expressions(filtername, filterargs)
end end
end end
@@ -79,31 +88,43 @@ module Liquid
end end
obj = context.apply_global_filter(obj) obj = context.apply_global_filter(obj)
taint_check(context, obj) taint_check(context, obj)
obj obj
end end
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
output
end
private private
def parse_filter_expressions(filter_name, unparsed_args) def parse_filter_expressions(filter_name, unparsed_args)
filter_args = [] filter_args = []
keyword_args = {} keyword_args = nil
unparsed_args.each do |a| unparsed_args.each do |a|
if matches = a.match(/\A#{TagAttributes}\z/o) if (matches = a.match(JUST_TAG_ATTRIBUTES))
keyword_args ||= {}
keyword_args[matches[1]] = Expression.parse(matches[2]) keyword_args[matches[1]] = Expression.parse(matches[2])
else else
filter_args << Expression.parse(a) filter_args << Expression.parse(a)
end end
end end
result = [filter_name, filter_args] result = [filter_name, filter_args]
result << keyword_args unless keyword_args.empty? result << keyword_args if keyword_args
result result
end end
def evaluate_filter_expressions(context, filter_args, filter_kwargs) def evaluate_filter_expressions(context, filter_args, filter_kwargs)
parsed_args = filter_args.map{ |expr| context.evaluate(expr) } parsed_args = filter_args.map { |expr| context.evaluate(expr) }
if filter_kwargs if filter_kwargs
parsed_kwargs = {} parsed_kwargs = {}
filter_kwargs.each do |key, expr| filter_kwargs.each do |key, expr|
@@ -118,7 +139,7 @@ module Liquid
return unless obj.tainted? return unless obj.tainted?
return if Template.taint_mode == :lax return if Template.taint_mode == :lax
@markup =~ QuotedFragment @markup =~ QUOTED_FRAGMENT
name = Regexp.last_match(0) name = Regexp.last_match(0)
error = TaintedError.new("variable '#{name}' is tainted and was not escaped") error = TaintedError.new("variable '#{name}' is tainted and was not escaped")
@@ -132,5 +153,11 @@ module Liquid
raise error raise error
end end
end end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[@node.name] + @node.filters.flatten
end
end
end end
end end

View File

@@ -1,7 +1,9 @@
# frozen_string_literal: true
module Liquid module Liquid
class VariableLookup class VariableLookup
SQUARE_BRACKETED = /\A\[(.*)\]\z/m SQUARE_BRACKETED = /\A\[(.*)\]\z/m.freeze
COMMAND_METHODS = ['size'.freeze, 'first'.freeze, 'last'.freeze] COMMAND_METHODS = %w[size first last].freeze
attr_reader :name, :lookups attr_reader :name, :lookups
@@ -10,12 +12,10 @@ module Liquid
end end
def initialize(markup) def initialize(markup)
lookups = markup.scan(VariableParser) lookups = markup.scan(VARIABLE_PARSER)
name = lookups.shift name = lookups.shift
if name =~ SQUARE_BRACKETED name = Expression.parse(Regexp.last_match(1)) if name =~ SQUARE_BRACKETED
name = Expression.parse($1)
end
@name = name @name = name
@lookups = lookups @lookups = lookups
@@ -24,7 +24,7 @@ module Liquid
@lookups.each_index do |i| @lookups.each_index do |i|
lookup = lookups[i] lookup = lookups[i]
if lookup =~ SQUARE_BRACKETED if lookup =~ SQUARE_BRACKETED
lookups[i] = Expression.parse($1) lookups[i] = Expression.parse(Regexp.last_match(1))
elsif COMMAND_METHODS.include?(lookup) elsif COMMAND_METHODS.include?(lookup)
@command_flags |= 1 << i @command_flags |= 1 << i
end end
@@ -41,8 +41,8 @@ module Liquid
# If object is a hash- or array-like object we look for the # If object is a hash- or array-like object we look for the
# presence of the key and if its available we return it # presence of the key and if its available we return it
if object.respond_to?(:[]) && if object.respond_to?(:[]) &&
((object.respond_to?(:key?) && object.key?(key)) || ((object.respond_to?(:key?) && object.key?(key)) ||
(object.respond_to?(:fetch) && key.is_a?(Integer))) (object.respond_to?(:fetch) && key.is_a?(Integer)))
# if its a proc we will replace the entry with the proc # if its a proc we will replace the entry with the proc
res = context.lookup_and_evaluate(object, key) res = context.lookup_and_evaluate(object, key)
@@ -59,6 +59,7 @@ module Liquid
# raise an exception if `strict_variables` option is set to true # raise an exception if `strict_variables` option is set to true
else else
return nil unless context.strict_variables return nil unless context.strict_variables
raise Liquid::UndefinedVariable, "undefined variable #{key}" raise Liquid::UndefinedVariable, "undefined variable #{key}"
end end
@@ -78,5 +79,11 @@ module Liquid
def state def state
[@name, @lookups, @command_flags] [@name, @lookups, @command_flags]
end end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
@node.lookups
end
end
end end
end end

View File

@@ -1,4 +1,5 @@
# encoding: utf-8 # frozen_string_literal: true
module Liquid module Liquid
VERSION = "4.0.0" VERSION = '4.0.3'
end end

View File

@@ -1,30 +1,31 @@
# encoding: utf-8 # frozen_string_literal: true
lib = File.expand_path('../lib/', __FILE__)
$LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib)
require "liquid/version" lib = File.expand_path('lib', __dir__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'liquid/version'
Gem::Specification.new do |s| Gem::Specification.new do |s|
s.name = "liquid" s.name = 'liquid'
s.version = Liquid::VERSION s.version = Liquid::VERSION
s.platform = Gem::Platform::RUBY s.platform = Gem::Platform::RUBY
s.summary = "A secure, non-evaling end user template engine with aesthetic markup." s.summary = 'A secure, non-evaling end user template engine with aesthetic markup.'
s.authors = ["Tobias Lütke"] s.authors = ['Tobias Lütke']
s.email = ["tobi@leetsoft.com"] s.email = ['tobi@leetsoft.com']
s.homepage = "http://www.liquidmarkup.org" s.homepage = 'http://www.liquidmarkup.org'
s.license = "MIT" s.license = 'MIT'
# s.description = "A secure, non-evaling end user template engine with aesthetic markup." # s.description = "A secure, non-evaling end user template engine with aesthetic markup."
s.required_ruby_version = ">= 2.1.0" s.required_ruby_version = '>= 2.4.0'
s.required_rubygems_version = ">= 1.3.7" s.required_rubygems_version = '>= 1.3.7'
s.test_files = Dir.glob("{test}/**/*") s.test_files = Dir.glob('{test}/**/*')
s.files = Dir.glob("{lib}/**/*") + %w(LICENSE README.md) s.files = Dir.glob('{lib}/**/*') + %w[LICENSE README.md]
s.extra_rdoc_files = ["History.md", "README.md"] s.extra_rdoc_files = ['History.md', 'README.md']
s.require_path = "lib" s.require_path = 'lib'
s.add_development_dependency 'rake', '~> 11.3' s.add_development_dependency('minitest')
s.add_development_dependency 'minitest' s.add_development_dependency('rake', '~> 11.3')
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'benchmark/ips' require 'benchmark/ips'
require_relative 'theme_runner' require_relative 'theme_runner'
@@ -12,7 +14,7 @@ Benchmark.ips do |x|
puts "Running benchmark for #{x.time} seconds (with #{x.warmup} seconds warmup)." puts "Running benchmark for #{x.time} seconds (with #{x.warmup} seconds warmup)."
puts puts
x.report("parse:") { profiler.compile } x.report('parse:') { profiler.compile }
x.report("render:") { profiler.render } x.report('render:') { profiler.render }
x.report("parse & render:") { profiler.run } x.report('parse & render:') { profiler.run }
end end

View File

@@ -0,0 +1,26 @@
# frozen_string_literal: true
require 'benchmark/ips'
require 'memory_profiler'
require_relative 'theme_runner'
def profile(phase, &block)
puts
puts "#{phase}:"
puts
report = MemoryProfiler.report(&block)
report.pretty_print(
color_output: true,
scale_bytes: true,
detailed_report: true
)
end
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new
profile('Parsing') { profiler.compile }
profile('Rendering') { profiler.render }

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'stackprof' require 'stackprof'
require_relative 'theme_runner' require_relative 'theme_runner'
@@ -5,7 +7,7 @@ Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new profiler = ThemeRunner.new
profiler.run profiler.run
[:cpu, :object].each do |profile_type| %i[cpu object].each do |profile_type|
puts "Profiling in #{profile_type} mode..." puts "Profiling in #{profile_type} mode..."
results = StackProf.run(mode: profile_type) do results = StackProf.run(mode: profile_type) do
200.times do 200.times do
@@ -13,12 +15,12 @@ profiler.run
end end
end end
if profile_type == :cpu && graph_filename = ENV['GRAPH_FILENAME'] if profile_type == :cpu && (graph_filename = ENV['GRAPH_FILENAME'])
File.open(graph_filename, 'w') do |f| File.open(graph_filename, 'w') do |f|
StackProf::Report.new(results).print_graphviz(nil, f) StackProf::Report.new(results).print_graphviz(nil, f)
end end
end end
StackProf::Report.new(results).print_text(false, 20) StackProf::Report.new(results).print_text(false, 20)
File.write(ENV['FILENAME'] + "." + profile_type.to_s, Marshal.dump(results)) if ENV['FILENAME'] File.write(ENV['FILENAME'] + '.' + profile_type.to_s, Marshal.dump(results)) if ENV['FILENAME']
end end

View File

@@ -1,18 +1,20 @@
# frozen_string_literal: true
class CommentForm < Liquid::Block class CommentForm < Liquid::Block
Syntax = /(#{Liquid::VariableSignature}+)/ SYNTAX = /(#{Liquid::VARIABLE_SIGNATURE}+)/.freeze
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
if markup =~ Syntax if markup =~ SYNTAX
@variable_name = $1 @variable_name = Regexp.last_match(1)
@attributes = {} @attributes = {}
else else
raise SyntaxError.new("Syntax Error in 'comment_form' - Valid syntax: comment_form [article]") raise SyntaxError, "Syntax Error in 'comment_form' - Valid syntax: comment_form [article]"
end end
end end
def render(context) def render_to_output_buffer(context, output)
article = context[@variable_name] article = context[@variable_name]
context.stack do context.stack do
@@ -20,10 +22,12 @@ class CommentForm < Liquid::Block
'posted_successfully?' => context.registers[:posted_successfully], 'posted_successfully?' => context.registers[:posted_successfully],
'errors' => context['comment.errors'], 'errors' => context['comment.errors'],
'author' => context['comment.author'], 'author' => context['comment.author'],
'email' => context['comment.email'], 'email' => context['comment.email'],
'body' => context['comment.body'] 'body' => context['comment.body'],
} }
wrap_in_form(article, render_all(@nodelist, context))
output << wrap_in_form(article, render_all(@nodelist, context, output))
output
end end
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'yaml' require 'yaml'
module Database module Database
@@ -16,8 +18,11 @@ module Database
end end
# key the tables by handles, as this is how liquid expects it. # key the tables by handles, as this is how liquid expects it.
db = db.inject({}) do |assigns, (key, values)| db = db.each_with_object({}) do |(key, values), assigns|
assigns[key] = values.inject({}) { |h, v| h[v['handle']] = v; h; } assigns[key] = values.each_with_object({}) do |v, h|
h[v['handle']] = v
h
end
assigns assigns
end end
@@ -29,9 +34,9 @@ module Database
db['article'] = db['blog']['articles'].first db['article'] = db['blog']['articles'].first
db['cart'] = { db['cart'] = {
'total_price' => db['line_items'].values.inject(0) { |sum, item| sum += item['line_price'] * item['quantity'] }, 'total_price' => db['line_items'].values.inject(0) { |sum, item| sum + item['line_price'] * item['quantity'] },
'item_count' => db['line_items'].values.inject(0) { |sum, item| sum += item['quantity'] }, 'item_count' => db['line_items'].values.inject(0) { |sum, item| sum + item['quantity'] },
'items' => db['line_items'].values 'items' => db['line_items'].values,
} }
db db
@@ -40,6 +45,6 @@ module Database
end end
if __FILE__ == $PROGRAM_NAME if __FILE__ == $PROGRAM_NAME
p Database.tables['collections']['frontpage'].keys p(Database.tables['collections']['frontpage'].keys)
# p Database.tables['blog']['articles'] # p Database.tables['blog']['articles']
end end

View File

@@ -1,7 +1,9 @@
# frozen_string_literal: true
require 'json' require 'json'
module JsonFilter module JsonFilter
def json(object) def json(object)
JSON.dump(object.reject { |k, v| k == "collections" }) JSON.dump(object.reject { |k, _v| k == 'collections' })
end end
end end

View File

@@ -1,4 +1,6 @@
$:.unshift __dir__ + '/../../lib' # frozen_string_literal: true
$LOAD_PATH.unshift(__dir__ + '/../../lib')
require_relative '../../lib/liquid' require_relative '../../lib/liquid'
require_relative 'comment_form' require_relative 'comment_form'
@@ -9,11 +11,11 @@ require_relative 'shop_filter'
require_relative 'tag_filter' require_relative 'tag_filter'
require_relative 'weight_filter' require_relative 'weight_filter'
Liquid::Template.register_tag 'paginate', Paginate Liquid::Template.register_tag('paginate', Paginate)
Liquid::Template.register_tag 'form', CommentForm Liquid::Template.register_tag('form', CommentForm)
Liquid::Template.register_filter JsonFilter Liquid::Template.register_filter(JsonFilter)
Liquid::Template.register_filter MoneyFilter Liquid::Template.register_filter(MoneyFilter)
Liquid::Template.register_filter WeightFilter Liquid::Template.register_filter(WeightFilter)
Liquid::Template.register_filter ShopFilter Liquid::Template.register_filter(ShopFilter)
Liquid::Template.register_filter TagFilter Liquid::Template.register_filter(TagFilter)

View File

@@ -1,12 +1,16 @@
# frozen_string_literal: true
module MoneyFilter module MoneyFilter
def money_with_currency(money) def money_with_currency(money)
return '' if money.nil? return '' if money.nil?
sprintf("$ %.2f USD", money / 100.0)
format('$ %.2f USD', money / 100.0)
end end
def money(money) def money(money)
return '' if money.nil? return '' if money.nil?
sprintf("$ %.2f", money / 100.0)
format('$ %.2f', money / 100.0)
end end
private private

View File

@@ -1,49 +1,51 @@
# frozen_string_literal: true
class Paginate < Liquid::Block class Paginate < Liquid::Block
Syntax = /(#{Liquid::QuotedFragment})\s*(by\s*(\d+))?/ SYNTAX = /(#{Liquid::QUOTED_FRAGMENT})\s*(by\s*(\d+))?/.freeze
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
if markup =~ Syntax if markup =~ SYNTAX
@collection_name = $1 @collection_name = Regexp.last_match(1)
@page_size = if $2 @page_size = if Regexp.last_match(2)
$3.to_i Regexp.last_match(3).to_i
else else
20 20
end end
@attributes = { 'window_size' => 3 } @attributes = { 'window_size' => 3 }
markup.scan(Liquid::TagAttributes) do |key, value| markup.scan(Liquid::TAG_ATTRIBUTES) do |key, value|
@attributes[key] = value @attributes[key] = value
end end
else else
raise SyntaxError.new("Syntax Error in tag 'paginate' - Valid syntax: paginate [collection] by number") raise SyntaxError, "Syntax Error in tag 'paginate' - Valid syntax: paginate [collection] by number"
end end
end end
def render(context) def render_to_output_buffer(context, output)
@context = context @context = context
context.stack do context.stack do
current_page = context['current_page'].to_i current_page = context['current_page'].to_i
pagination = { pagination = {
'page_size' => @page_size, 'page_size' => @page_size,
'current_page' => 5, 'current_page' => 5,
'current_offset' => @page_size * 5 'current_offset' => @page_size * 5,
} }
context['paginate'] = pagination context['paginate'] = pagination
collection_size = context[@collection_name].size collection_size = context[@collection_name].size
raise ArgumentError.new("Cannot paginate array '#{@collection_name}'. Not found.") if collection_size.nil? raise ArgumentError, "Cannot paginate array '#{@collection_name}'. Not found." if collection_size.nil?
page_count = (collection_size.to_f / @page_size.to_f).to_f.ceil + 1 page_count = (collection_size.to_f / @page_size.to_f).to_f.ceil + 1
pagination['items'] = collection_size pagination['items'] = collection_size
pagination['pages'] = page_count - 1 pagination['pages'] = page_count - 1
pagination['previous'] = link('&laquo; Previous', current_page - 1) unless 1 >= current_page pagination['previous'] = link('&laquo; Previous', current_page - 1) unless current_page <= 1
pagination['next'] = link('Next &raquo;', current_page + 1) unless page_count <= current_page + 1 pagination['next'] = link('Next &raquo;', current_page + 1) unless page_count <= current_page + 1
pagination['parts'] = [] pagination['parts'] = []
@@ -59,6 +61,7 @@ class Paginate < Liquid::Block
pagination['parts'] << link(page, page) pagination['parts'] << link(page, page)
elsif page <= current_page - @attributes['window_size'] || page >= current_page + @attributes['window_size'] elsif page <= current_page - @attributes['window_size'] || page >= current_page + @attributes['window_size']
next if hellip_break next if hellip_break
pagination['parts'] << no_link('&hellip;') pagination['parts'] << no_link('&hellip;')
hellip_break = true hellip_break = true
next next
@@ -85,6 +88,6 @@ class Paginate < Liquid::Block
end end
def current_url def current_url
"/collections/frontpage" '/collections/frontpage'
end end
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module ShopFilter module ShopFilter
def asset_url(input) def asset_url(input)
"/files/1/[shop_id]/[shop_id]/assets/#{input}" "/files/1/[shop_id]/[shop_id]/assets/#{input}"
@@ -15,15 +17,15 @@ module ShopFilter
%(<script src="#{url}" type="text/javascript"></script>) %(<script src="#{url}" type="text/javascript"></script>)
end end
def stylesheet_tag(url, media = "all") def stylesheet_tag(url, media = 'all')
%(<link href="#{url}" rel="stylesheet" type="text/css" media="#{media}" />) %(<link href="#{url}" rel="stylesheet" type="text/css" media="#{media}" />)
end end
def link_to(link, url, title = "") def link_to(link, url, title = '')
%(<a href="#{url}" title="#{title}">#{link}</a>) %(<a href="#{url}" title="#{title}">#{link}</a>)
end end
def img_tag(url, alt = "") def img_tag(url, alt = '')
%(<img src="#{url}" alt="#{alt}" />) %(<img src="#{url}" alt="#{alt}" />)
end end
@@ -52,15 +54,13 @@ module ShopFilter
end end
def product_img_url(url, style = 'small') def product_img_url(url, style = 'small')
unless url =~ /\Aproducts\/([\w\-\_]+)\.(\w{2,4})/ raise ArgumentError, 'filter "size" can only be called on product images' unless url =~ %r{\Aproducts/([\w\-\_]+)\.(\w{2,4})}
raise ArgumentError, 'filter "size" can only be called on product images'
end
case style case style
when 'original' when 'original'
return '/files/shops/random_number/' + url return '/files/shops/random_number/' + url
when 'grande', 'large', 'medium', 'compact', 'small', 'thumb', 'icon' when 'grande', 'large', 'medium', 'compact', 'small', 'thumb', 'icon'
"/files/shops/random_number/products/#{$1}_#{style}.#{$2}" "/files/shops/random_number/products/#{Regexp.last_match(1)}_#{style}.#{Regexp.last_match(2)}"
else else
raise ArgumentError, 'valid parameters for filter "size" are: original, grande, large, medium, compact, small, thumb and icon ' raise ArgumentError, 'valid parameters for filter "size" are: original, grande, large, medium, compact, small, thumb and icon '
end end
@@ -70,16 +70,14 @@ module ShopFilter
html = [] html = []
html << %(<span class="prev">#{link_to(paginate['previous']['title'], paginate['previous']['url'])}</span>) if paginate['previous'] html << %(<span class="prev">#{link_to(paginate['previous']['title'], paginate['previous']['url'])}</span>) if paginate['previous']
for part in paginate['parts'] paginate['parts'].each do |part|
html << if part['is_link']
if part['is_link'] %(<span class="page">#{link_to(part['title'], part['url'])}</span>)
html << %(<span class="page">#{link_to(part['title'], part['url'])}</span>)
elsif part['title'].to_i == paginate['current_page'].to_i elsif part['title'].to_i == paginate['current_page'].to_i
html << %(<span class="page current">#{part['title']}</span>) %(<span class="page current">#{part['title']}</span>)
else else
html << %(<span class="deco">#{part['title']}</span>) %(<span class="deco">#{part['title']}</span>)
end end
end end
html << %(<span class="next">#{link_to(paginate['next']['title'], paginate['next']['url'])}</span>) if paginate['next'] html << %(<span class="next">#{link_to(paginate['next']['title'], paginate['next']['url'])}</span>) if paginate['next']

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module TagFilter module TagFilter
def link_to_tag(label, tag) def link_to_tag(label, tag)
"<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tag}\">#{label}</a>" "<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tag}\">#{label}</a>"
@@ -13,11 +15,11 @@ module TagFilter
def link_to_add_tag(label, tag) def link_to_add_tag(label, tag)
tags = (@context['current_tags'] + [tag]).uniq tags = (@context['current_tags'] + [tag]).uniq
"<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tags.join("+")}\">#{label}</a>" "<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tags.join('+')}\">#{label}</a>"
end end
def link_to_remove_tag(label, tag) def link_to_remove_tag(label, tag)
tags = (@context['current_tags'] - [tag]).uniq tags = (@context['current_tags'] - [tag]).uniq
"<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tags.join("+")}\">#{label}</a>" "<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tags.join('+')}\">#{label}</a>"
end end
end end

View File

@@ -1,6 +1,8 @@
# frozen_string_literal: true
module WeightFilter module WeightFilter
def weight(grams) def weight(grams)
sprintf("%.2f", grams / 1000) format('%.2f', grams / 1000)
end end
def weight_with_unit(grams) def weight_with_unit(grams)

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
# This profiler run simulates Shopify. # This profiler run simulates Shopify.
# We are looking in the tests directory for liquid files and render them within the designated layout file. # We are looking in the tests directory for liquid files and render them within the designated layout file.
# We will also export a substantial database to liquid which the templates can render values of. # We will also export a substantial database to liquid which the templates can render values of.
@@ -31,7 +33,7 @@ class ThemeRunner
{ {
liquid: File.read(test), liquid: File.read(test),
layout: (File.file?(theme_path) ? File.read(theme_path) : nil), layout: (File.file?(theme_path) ? File.read(theme_path) : nil),
template_name: test template_name: test,
} }
end.compact end.compact

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class AssignTest < Minitest::Test class AssignTest < Minitest::Test
@@ -10,29 +12,29 @@ class AssignTest < Minitest::Test
END_TEMPLATE END_TEMPLATE
template = Template.parse(template_source) template = Template.parse(template_source)
rendered = template.render! rendered = template.render!
assert_equal "Print this-thing", rendered.strip assert_equal 'Print this-thing', rendered.strip
end end
def test_assigned_variable def test_assigned_variable
assert_template_result('.foo.', assert_template_result('.foo.',
'{% assign foo = values %}.{{ foo[0] }}.', '{% assign foo = values %}.{{ foo[0] }}.',
'values' => %w(foo bar baz)) 'values' => %w[foo bar baz])
assert_template_result('.bar.', assert_template_result('.bar.',
'{% assign foo = values %}.{{ foo[1] }}.', '{% assign foo = values %}.{{ foo[1] }}.',
'values' => %w(foo bar baz)) 'values' => %w[foo bar baz])
end end
def test_assign_with_filter def test_assign_with_filter
assert_template_result('.bar.', assert_template_result('.bar.',
'{% assign foo = values | split: "," %}.{{ foo[1] }}.', '{% assign foo = values | split: "," %}.{{ foo[1] }}.',
'values' => "foo,bar,baz") 'values' => 'foo,bar,baz')
end end
def test_assign_syntax_error def test_assign_syntax_error
assert_match_syntax_error(/assign/, assert_match_syntax_error(/assign/,
'{% assign foo not values %}.', '{% assign foo not values %}.',
'values' => "foo,bar,baz") 'values' => 'foo,bar,baz')
end end
def test_assign_uses_error_mode def test_assign_uses_error_mode

View File

@@ -1,11 +1,12 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class FoobarTag < Liquid::Tag class FoobarTag < Liquid::Tag
def render(*args) def render_to_output_buffer(_context, output)
" " output << ' '
output
end end
Liquid::Template.register_tag('foobar', FoobarTag)
end end
class BlankTestFileSystem class BlankTestFileSystem
@@ -31,76 +32,78 @@ class BlankTest < Minitest::Test
end end
def test_new_tags_are_not_blank_by_default def test_new_tags_are_not_blank_by_default
assert_template_result(" " * N, wrap_in_for("{% foobar %}")) with_custom_tag('foobar', FoobarTag) do
assert_template_result(' ' * N, wrap_in_for('{% foobar %}'))
end
end end
def test_loops_are_blank def test_loops_are_blank
assert_template_result("", wrap_in_for(" ")) assert_template_result('', wrap_in_for(' '))
end end
def test_if_else_are_blank def test_if_else_are_blank
assert_template_result("", "{% if true %} {% elsif false %} {% else %} {% endif %}") assert_template_result('', '{% if true %} {% elsif false %} {% else %} {% endif %}')
end end
def test_unless_is_blank def test_unless_is_blank
assert_template_result("", wrap("{% unless true %} {% endunless %}")) assert_template_result('', wrap('{% unless true %} {% endunless %}'))
end end
def test_mark_as_blank_only_during_parsing def test_mark_as_blank_only_during_parsing
assert_template_result(" " * (N + 1), wrap(" {% if false %} this never happens, but still, this block is not blank {% endif %}")) assert_template_result(' ' * (N + 1), wrap(' {% if false %} this never happens, but still, this block is not blank {% endif %}'))
end end
def test_comments_are_blank def test_comments_are_blank
assert_template_result("", wrap(" {% comment %} whatever {% endcomment %} ")) assert_template_result('', wrap(' {% comment %} whatever {% endcomment %} '))
end end
def test_captures_are_blank def test_captures_are_blank
assert_template_result("", wrap(" {% capture foo %} whatever {% endcapture %} ")) assert_template_result('', wrap(' {% capture foo %} whatever {% endcapture %} '))
end end
def test_nested_blocks_are_blank_but_only_if_all_children_are def test_nested_blocks_are_blank_but_only_if_all_children_are
assert_template_result("", wrap(wrap(" "))) assert_template_result('', wrap(wrap(' ')))
assert_template_result("\n but this is not " * (N + 1), assert_template_result("\n but this is not " * (N + 1),
wrap('{% if true %} {% comment %} this is blank {% endcomment %} {% endif %} wrap('{% if true %} {% comment %} this is blank {% endcomment %} {% endif %}
{% if true %} but this is not {% endif %}')) {% if true %} but this is not {% endif %}'))
end end
def test_assigns_are_blank def test_assigns_are_blank
assert_template_result("", wrap(' {% assign foo = "bar" %} ')) assert_template_result('', wrap(' {% assign foo = "bar" %} '))
end end
def test_whitespace_is_blank def test_whitespace_is_blank
assert_template_result("", wrap(" ")) assert_template_result('', wrap(' '))
assert_template_result("", wrap("\t")) assert_template_result('', wrap("\t"))
end end
def test_whitespace_is_not_blank_if_other_stuff_is_present def test_whitespace_is_not_blank_if_other_stuff_is_present
body = " x " body = ' x '
assert_template_result(body * (N + 1), wrap(body)) assert_template_result(body * (N + 1), wrap(body))
end end
def test_increment_is_not_blank def test_increment_is_not_blank
assert_template_result(" 0" * 2 * (N + 1), wrap("{% assign foo = 0 %} {% increment foo %} {% decrement foo %}")) assert_template_result(' 0' * 2 * (N + 1), wrap('{% assign foo = 0 %} {% increment foo %} {% decrement foo %}'))
end end
def test_cycle_is_not_blank def test_cycle_is_not_blank
assert_template_result(" " * ((N + 1) / 2) + " ", wrap("{% cycle ' ', ' ' %}")) assert_template_result(' ' * ((N + 1) / 2) + ' ', wrap("{% cycle ' ', ' ' %}"))
end end
def test_raw_is_not_blank def test_raw_is_not_blank
assert_template_result(" " * (N + 1), wrap(" {% raw %} {% endraw %}")) assert_template_result(' ' * (N + 1), wrap(' {% raw %} {% endraw %}'))
end end
def test_include_is_blank def test_include_is_blank
Liquid::Template.file_system = BlankTestFileSystem.new Liquid::Template.file_system = BlankTestFileSystem.new
assert_template_result "foobar" * (N + 1), wrap("{% include 'foobar' %}") assert_template_result 'foobar' * (N + 1), wrap("{% include 'foobar' %}")
assert_template_result " foobar " * (N + 1), wrap("{% include ' foobar ' %}") assert_template_result ' foobar ' * (N + 1), wrap("{% include ' foobar ' %}")
assert_template_result " " * (N + 1), wrap(" {% include ' ' %} ") assert_template_result ' ' * (N + 1), wrap(" {% include ' ' %} ")
end end
def test_case_is_blank def test_case_is_blank
assert_template_result("", wrap(" {% assign foo = 'bar' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} {% endcase %} ")) assert_template_result('', wrap(" {% assign foo = 'bar' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} {% endcase %} "))
assert_template_result("", wrap(" {% assign foo = 'else' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} {% endcase %} ")) assert_template_result('', wrap(" {% assign foo = 'else' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} {% endcase %} "))
assert_template_result(" x " * (N + 1), wrap(" {% assign foo = 'else' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} x {% endcase %} ")) assert_template_result(' x ' * (N + 1), wrap(" {% assign foo = 'else' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} x {% endcase %} "))
end end
end end

View File

@@ -0,0 +1,14 @@
# frozen_string_literal: true
require 'test_helper'
class BlockTest < Minitest::Test
include Liquid
def test_unexpected_end_tag
exc = assert_raises(SyntaxError) do
Template.parse('{% if true %}{% endunless %}')
end
assert_equal exc.message, "Liquid syntax error: 'endunless' is not a valid delimiter for if tags. use endif"
end
end

View File

@@ -1,10 +1,12 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class CaptureTest < Minitest::Test class CaptureTest < Minitest::Test
include Liquid include Liquid
def test_captures_block_content_in_variable def test_captures_block_content_in_variable
assert_template_result("test string", "{% capture 'var' %}test string{% endcapture %}{{var}}", {}) assert_template_result('test string', "{% capture 'var' %}test string{% endcapture %}{{var}}", {})
end end
def test_capture_with_hyphen_in_variable_name def test_capture_with_hyphen_in_variable_name
@@ -14,7 +16,7 @@ class CaptureTest < Minitest::Test
END_TEMPLATE END_TEMPLATE
template = Template.parse(template_source) template = Template.parse(template_source)
rendered = template.render! rendered = template.render!
assert_equal "Print this-thing", rendered.strip assert_equal 'Print this-thing', rendered.strip
end end
def test_capture_to_variable_from_outer_scope_if_existing def test_capture_to_variable_from_outer_scope_if_existing
@@ -30,7 +32,7 @@ class CaptureTest < Minitest::Test
END_TEMPLATE END_TEMPLATE
template = Template.parse(template_source) template = Template.parse(template_source)
rendered = template.render! rendered = template.render!
assert_equal "test-string", rendered.gsub(/\s/, '') assert_equal 'test-string', rendered.gsub(/\s/, '')
end end
def test_assigning_from_capture def test_assigning_from_capture
@@ -45,6 +47,6 @@ class CaptureTest < Minitest::Test
END_TEMPLATE END_TEMPLATE
template = Template.parse(template_source) template = Template.parse(template_source)
rendered = template.render! rendered = template.render!
assert_equal "3-3", rendered.gsub(/\s/, '') assert_equal '3-3', rendered.gsub(/\s/, '')
end end
end # CaptureTest end # CaptureTest

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class ContextTest < Minitest::Test class ContextTest < Minitest::Test

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class DocumentTest < Minitest::Test class DocumentTest < Minitest::Test
@@ -5,14 +7,14 @@ class DocumentTest < Minitest::Test
def test_unexpected_outer_tag def test_unexpected_outer_tag
exc = assert_raises(SyntaxError) do exc = assert_raises(SyntaxError) do
Template.parse("{% else %}") Template.parse('{% else %}')
end end
assert_equal exc.message, "Liquid syntax error: Unexpected outer 'else' tag" assert_equal exc.message, "Liquid syntax error: Unexpected outer 'else' tag"
end end
def test_unknown_tag def test_unknown_tag
exc = assert_raises(SyntaxError) do exc = assert_raises(SyntaxError) do
Template.parse("{% foo %}") Template.parse('{% foo %}')
end end
assert_equal exc.message, "Liquid syntax error: Unknown tag 'foo'" assert_equal exc.message, "Liquid syntax error: Unknown tag 'foo'"
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class ContextDrop < Liquid::Drop class ContextDrop < Liquid::Drop
@@ -21,7 +23,7 @@ end
class ProductDrop < Liquid::Drop class ProductDrop < Liquid::Drop
class TextDrop < Liquid::Drop class TextDrop < Liquid::Drop
def array def array
['text1', 'text2'] %w[text1 text2]
end end
def text def text
@@ -31,7 +33,7 @@ class ProductDrop < Liquid::Drop
class CatchallDrop < Liquid::Drop class CatchallDrop < Liquid::Drop
def liquid_method_missing(method) def liquid_method_missing(method)
'catchall_method: ' << method.to_s +'catchall_method: ' << method.to_s
end end
end end
@@ -48,13 +50,13 @@ class ProductDrop < Liquid::Drop
end end
def user_input def user_input
"foo".taint (+'foo').taint
end end
protected protected
def callmenot def callmenot
"protected" 'protected'
end end
end end
@@ -139,17 +141,17 @@ class DropsTest < Minitest::Test
end end
def test_drop_does_only_respond_to_whitelisted_methods def test_drop_does_only_respond_to_whitelisted_methods
assert_equal "", Liquid::Template.parse("{{ product.inspect }}").render!('product' => ProductDrop.new) assert_equal '', Liquid::Template.parse('{{ product.inspect }}').render!('product' => ProductDrop.new)
assert_equal "", Liquid::Template.parse("{{ product.pretty_inspect }}").render!('product' => ProductDrop.new) assert_equal '', Liquid::Template.parse('{{ product.pretty_inspect }}').render!('product' => ProductDrop.new)
assert_equal "", Liquid::Template.parse("{{ product.whatever }}").render!('product' => ProductDrop.new) assert_equal '', Liquid::Template.parse('{{ product.whatever }}').render!('product' => ProductDrop.new)
assert_equal "", Liquid::Template.parse('{{ product | map: "inspect" }}').render!('product' => ProductDrop.new) assert_equal '', Liquid::Template.parse('{{ product | map: "inspect" }}').render!('product' => ProductDrop.new)
assert_equal "", Liquid::Template.parse('{{ product | map: "pretty_inspect" }}').render!('product' => ProductDrop.new) assert_equal '', Liquid::Template.parse('{{ product | map: "pretty_inspect" }}').render!('product' => ProductDrop.new)
assert_equal "", Liquid::Template.parse('{{ product | map: "whatever" }}').render!('product' => ProductDrop.new) assert_equal '', Liquid::Template.parse('{{ product | map: "whatever" }}').render!('product' => ProductDrop.new)
end end
def test_drops_respond_to_to_liquid def test_drops_respond_to_to_liquid
assert_equal "text1", Liquid::Template.parse("{{ product.to_liquid.texts.text }}").render!('product' => ProductDrop.new) assert_equal 'text1', Liquid::Template.parse('{{ product.to_liquid.texts.text }}').render!('product' => ProductDrop.new)
assert_equal "text1", Liquid::Template.parse('{{ product | map: "to_liquid" | map: "texts" | map: "text" }}').render!('product' => ProductDrop.new) assert_equal 'text1', Liquid::Template.parse('{{ product | map: "to_liquid" | map: "texts" | map: "text" }}').render!('product' => ProductDrop.new)
end end
def test_text_drop def test_text_drop
@@ -173,12 +175,12 @@ class DropsTest < Minitest::Test
end end
def test_context_drop def test_context_drop
output = Liquid::Template.parse(' {{ context.bar }} ').render!('context' => ContextDrop.new, 'bar' => "carrot") output = Liquid::Template.parse(' {{ context.bar }} ').render!('context' => ContextDrop.new, 'bar' => 'carrot')
assert_equal ' carrot ', output assert_equal ' carrot ', output
end end
def test_nested_context_drop def test_nested_context_drop
output = Liquid::Template.parse(' {{ product.context.foo }} ').render!('product' => ProductDrop.new, 'foo' => "monkey") output = Liquid::Template.parse(' {{ product.context.foo }} ').render!('product' => ProductDrop.new, 'foo' => 'monkey')
assert_equal ' monkey ', output assert_equal ' monkey ', output
end end
@@ -188,7 +190,7 @@ class DropsTest < Minitest::Test
end end
def test_object_methods_not_allowed def test_object_methods_not_allowed
[:dup, :clone, :singleton_class, :eval, :class_eval, :inspect].each do |method| %i[dup clone singleton_class eval class_eval inspect].each do |method|
output = Liquid::Template.parse(" {{ product.#{method} }} ").render!('product' => ProductDrop.new) output = Liquid::Template.parse(" {{ product.#{method} }} ").render!('product' => ProductDrop.new)
assert_equal ' ', output assert_equal ' ', output
end end
@@ -201,9 +203,9 @@ class DropsTest < Minitest::Test
end end
def test_scope_though_proc def test_scope_though_proc
assert_equal '1', Liquid::Template.parse('{{ s }}').render!('context' => ContextDrop.new, 's' => proc{ |c| c['context.scopes'] }) assert_equal '1', Liquid::Template.parse('{{ s }}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] })
assert_equal '2', Liquid::Template.parse('{%for i in dummy%}{{ s }}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc{ |c| c['context.scopes'] }, 'dummy' => [1]) assert_equal '2', Liquid::Template.parse('{%for i in dummy%}{{ s }}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }, 'dummy' => [1])
assert_equal '3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ s }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc{ |c| c['context.scopes'] }, 'dummy' => [1]) assert_equal '3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ s }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }, 'dummy' => [1])
end end
def test_scope_with_assigns def test_scope_with_assigns
@@ -232,7 +234,7 @@ class DropsTest < Minitest::Test
end end
def test_enumerable_drop_will_invoke_liquid_method_missing_for_clashing_method_names def test_enumerable_drop_will_invoke_liquid_method_missing_for_clashing_method_names
["select", "each", "map", "cycle"].each do |method| %w[select each map cycle].each do |method|
assert_equal method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new) assert_equal method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)
assert_equal method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new) assert_equal method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)
assert_equal method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new) assert_equal method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new)
@@ -241,20 +243,20 @@ class DropsTest < Minitest::Test
end end
def test_some_enumerable_methods_still_get_invoked def test_some_enumerable_methods_still_get_invoked
[ :count, :max ].each do |method| %i[count max].each do |method|
assert_equal "3", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new) assert_equal '3', Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new)
assert_equal "3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new) assert_equal '3', Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new)
assert_equal "3", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new) assert_equal '3', Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)
assert_equal "3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new) assert_equal '3', Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)
end end
assert_equal "yes", Liquid::Template.parse("{% if collection contains 3 %}yes{% endif %}").render!('collection' => RealEnumerableDrop.new) assert_equal 'yes', Liquid::Template.parse('{% if collection contains 3 %}yes{% endif %}').render!('collection' => RealEnumerableDrop.new)
[ :min, :first ].each do |method| %i[min first].each do |method|
assert_equal "1", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new) assert_equal '1', Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new)
assert_equal "1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new) assert_equal '1', Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new)
assert_equal "1", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new) assert_equal '1', Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)
assert_equal "1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new) assert_equal '1', Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)
end end
end end
@@ -267,7 +269,7 @@ class DropsTest < Minitest::Test
end end
def test_default_to_s_on_drops def test_default_to_s_on_drops
assert_equal 'ProductDrop', Liquid::Template.parse("{{ product }}").render!('product' => ProductDrop.new) assert_equal 'ProductDrop', Liquid::Template.parse('{{ product }}').render!('product' => ProductDrop.new)
assert_equal 'EnumerableDrop', Liquid::Template.parse('{{ collection }}').render!('collection' => EnumerableDrop.new) assert_equal 'EnumerableDrop', Liquid::Template.parse('{{ collection }}').render!('collection' => EnumerableDrop.new)
end end
end # DropsTest end # DropsTest

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class ErrorHandlingTest < Minitest::Test class ErrorHandlingTest < Minitest::Test
@@ -83,15 +85,14 @@ class ErrorHandlingTest < Minitest::Test
def test_with_line_numbers_adds_numbers_to_parser_errors def test_with_line_numbers_adds_numbers_to_parser_errors
err = assert_raises(SyntaxError) do err = assert_raises(SyntaxError) do
Liquid::Template.parse(%q( Liquid::Template.parse('
foobar foobar
{% "cat" | foobar %} {% "cat" | foobar %}
bla bla
), ',
line_numbers: true line_numbers: true)
)
end end
assert_match(/Liquid syntax error \(line 4\)/, err.message) assert_match(/Liquid syntax error \(line 4\)/, err.message)
@@ -99,15 +100,14 @@ class ErrorHandlingTest < Minitest::Test
def test_with_line_numbers_adds_numbers_to_parser_errors_with_whitespace_trim def test_with_line_numbers_adds_numbers_to_parser_errors_with_whitespace_trim
err = assert_raises(SyntaxError) do err = assert_raises(SyntaxError) do
Liquid::Template.parse(%q( Liquid::Template.parse('
foobar foobar
{%- "cat" | foobar -%} {%- "cat" | foobar -%}
bla bla
), ',
line_numbers: true line_numbers: true)
)
end end
assert_match(/Liquid syntax error \(line 4\)/, err.message) assert_match(/Liquid syntax error \(line 4\)/, err.message)
@@ -121,12 +121,11 @@ class ErrorHandlingTest < Minitest::Test
bla bla
', ',
error_mode: :warn, error_mode: :warn,
line_numbers: true line_numbers: true)
)
assert_equal ['Liquid syntax error (line 4): Unexpected character = in "1 =! 2"'], assert_equal ['Liquid syntax error (line 4): Unexpected character = in "1 =! 2"'],
template.warnings.map(&:message) template.warnings.map(&:message)
end end
def test_parsing_strict_with_line_numbers_adds_numbers_to_lexer_errors def test_parsing_strict_with_line_numbers_adds_numbers_to_lexer_errors
@@ -138,9 +137,8 @@ class ErrorHandlingTest < Minitest::Test
bla bla
', ',
error_mode: :strict, error_mode: :strict,
line_numbers: true line_numbers: true)
)
end end
assert_equal 'Liquid syntax error (line 4): Unexpected character = in "1 =! 2"', err.message assert_equal 'Liquid syntax error (line 4): Unexpected character = in "1 =! 2"', err.message
@@ -157,8 +155,7 @@ class ErrorHandlingTest < Minitest::Test
bla bla
', ',
line_numbers: true line_numbers: true)
)
end end
assert_equal "Liquid syntax error (line 5): Unknown tag 'foo'", err.message assert_equal "Liquid syntax error (line 5): Unknown tag 'foo'", err.message
@@ -205,7 +202,7 @@ class ErrorHandlingTest < Minitest::Test
def test_default_exception_renderer_with_internal_error def test_default_exception_renderer_with_internal_error
template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true) template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true)
output = template.render({ 'errors' => ErrorDrop.new }) output = template.render('errors' => ErrorDrop.new)
assert_equal 'This is a runtime error: Liquid error (line 1): internal', output assert_equal 'This is a runtime error: Liquid error (line 1): internal', output
assert_equal [Liquid::InternalError], template.errors.map(&:class) assert_equal [Liquid::InternalError], template.errors.map(&:class)
@@ -214,10 +211,13 @@ class ErrorHandlingTest < Minitest::Test
def test_setting_default_exception_renderer def test_setting_default_exception_renderer
old_exception_renderer = Liquid::Template.default_exception_renderer old_exception_renderer = Liquid::Template.default_exception_renderer
exceptions = [] exceptions = []
Liquid::Template.default_exception_renderer = ->(e) { exceptions << e; '' } Liquid::Template.default_exception_renderer = lambda { |e|
exceptions << e
''
}
template = Liquid::Template.parse('This is a runtime error: {{ errors.argument_error }}') template = Liquid::Template.parse('This is a runtime error: {{ errors.argument_error }}')
output = template.render({ 'errors' => ErrorDrop.new }) output = template.render('errors' => ErrorDrop.new)
assert_equal 'This is a runtime error: ', output assert_equal 'This is a runtime error: ', output
assert_equal [Liquid::ArgumentError], template.errors.map(&:class) assert_equal [Liquid::ArgumentError], template.errors.map(&:class)
@@ -228,7 +228,10 @@ class ErrorHandlingTest < Minitest::Test
def test_exception_renderer_exposing_non_liquid_error def test_exception_renderer_exposing_non_liquid_error
template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true) template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true)
exceptions = [] exceptions = []
handler = ->(e) { exceptions << e; e.cause } handler = lambda { |e|
exceptions << e
e.cause
}
output = template.render({ 'errors' => ErrorDrop.new }, exception_renderer: handler) output = template.render({ 'errors' => ErrorDrop.new }, exception_renderer: handler)
@@ -239,8 +242,8 @@ class ErrorHandlingTest < Minitest::Test
end end
class TestFileSystem class TestFileSystem
def read_template_file(template_path) def read_template_file(_template_path)
"{{ errors.argument_error }}" '{{ errors.argument_error }}'
end end
end end
@@ -255,6 +258,6 @@ class ErrorHandlingTest < Minitest::Test
Liquid::Template.file_system = old_file_system Liquid::Template.file_system = old_file_system
end end
assert_equal "Argument error:\nLiquid error (product line 1): argument error", page assert_equal "Argument error:\nLiquid error (product line 1): argument error", page
assert_equal "product", template.errors.first.template_name assert_equal 'product', template.errors.first.template_name
end end
end end

View File

@@ -1,24 +1,26 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
module MoneyFilter module MoneyFilter
def money(input) def money(input)
sprintf(' %d$ ', input) format(' %d$ ', input)
end end
def money_with_underscore(input) def money_with_underscore(input)
sprintf(' %d$ ', input) format(' %d$ ', input)
end end
end end
module CanadianMoneyFilter module CanadianMoneyFilter
def money(input) def money(input)
sprintf(' %d$ CAD ', input) format(' %d$ CAD ', input)
end end
end end
module SubstituteFilter module SubstituteFilter
def substitute(input, params = {}) def substitute(input, params = {})
input.gsub(/%\{(\w+)\}/) { |match| params[$1] } input.gsub(/%\{(\w+)\}/) { |_match| params[Regexp.last_match(1)] }
end end
end end
@@ -26,8 +28,8 @@ class FiltersTest < Minitest::Test
include Liquid include Liquid
module OverrideObjectMethodFilter module OverrideObjectMethodFilter
def tap(input) def tap(_input)
"tap overridden" 'tap overridden'
end end
end end
@@ -39,13 +41,13 @@ class FiltersTest < Minitest::Test
@context['var'] = 1000 @context['var'] = 1000
@context.add_filters(MoneyFilter) @context.add_filters(MoneyFilter)
assert_equal ' 1000$ ', Template.parse("{{var | money}}").render(@context) assert_equal ' 1000$ ', Template.parse('{{var | money}}').render(@context)
end end
def test_underscore_in_filter_name def test_underscore_in_filter_name
@context['var'] = 1000 @context['var'] = 1000
@context.add_filters(MoneyFilter) @context.add_filters(MoneyFilter)
assert_equal ' 1000$ ', Template.parse("{{var | money_with_underscore}}").render(@context) assert_equal ' 1000$ ', Template.parse('{{var | money_with_underscore}}').render(@context)
end end
def test_second_filter_overwrites_first def test_second_filter_overwrites_first
@@ -53,43 +55,43 @@ class FiltersTest < Minitest::Test
@context.add_filters(MoneyFilter) @context.add_filters(MoneyFilter)
@context.add_filters(CanadianMoneyFilter) @context.add_filters(CanadianMoneyFilter)
assert_equal ' 1000$ CAD ', Template.parse("{{var | money}}").render(@context) assert_equal ' 1000$ CAD ', Template.parse('{{var | money}}').render(@context)
end end
def test_size def test_size
@context['var'] = 'abcd' @context['var'] = 'abcd'
@context.add_filters(MoneyFilter) @context.add_filters(MoneyFilter)
assert_equal '4', Template.parse("{{var | size}}").render(@context) assert_equal '4', Template.parse('{{var | size}}').render(@context)
end end
def test_join def test_join
@context['var'] = [1, 2, 3, 4] @context['var'] = [1, 2, 3, 4]
assert_equal "1 2 3 4", Template.parse("{{var | join}}").render(@context) assert_equal '1 2 3 4', Template.parse('{{var | join}}').render(@context)
end end
def test_sort def test_sort
@context['value'] = 3 @context['value'] = 3
@context['numbers'] = [2, 1, 4, 3] @context['numbers'] = [2, 1, 4, 3]
@context['words'] = ['expected', 'as', 'alphabetic'] @context['words'] = %w[expected as alphabetic]
@context['arrays'] = ['flower', 'are'] @context['arrays'] = %w[flower are]
@context['case_sensitive'] = ['sensitive', 'Expected', 'case'] @context['case_sensitive'] = %w[sensitive Expected case]
assert_equal '1 2 3 4', Template.parse("{{numbers | sort | join}}").render(@context) assert_equal '1 2 3 4', Template.parse('{{numbers | sort | join}}').render(@context)
assert_equal 'alphabetic as expected', Template.parse("{{words | sort | join}}").render(@context) assert_equal 'alphabetic as expected', Template.parse('{{words | sort | join}}').render(@context)
assert_equal '3', Template.parse("{{value | sort}}").render(@context) assert_equal '3', Template.parse('{{value | sort}}').render(@context)
assert_equal 'are flower', Template.parse("{{arrays | sort | join}}").render(@context) assert_equal 'are flower', Template.parse('{{arrays | sort | join}}').render(@context)
assert_equal 'Expected case sensitive', Template.parse("{{case_sensitive | sort | join}}").render(@context) assert_equal 'Expected case sensitive', Template.parse('{{case_sensitive | sort | join}}').render(@context)
end end
def test_sort_natural def test_sort_natural
@context['words'] = ['case', 'Assert', 'Insensitive'] @context['words'] = %w[case Assert Insensitive]
@context['hashes'] = [{ 'a' => 'A' }, { 'a' => 'b' }, { 'a' => 'C' }] @context['hashes'] = [{ 'a' => 'A' }, { 'a' => 'b' }, { 'a' => 'C' }]
@context['objects'] = [TestObject.new('A'), TestObject.new('b'), TestObject.new('C')] @context['objects'] = [TestObject.new('A'), TestObject.new('b'), TestObject.new('C')]
# Test strings # Test strings
assert_equal 'Assert case Insensitive', Template.parse("{{words | sort_natural | join}}").render(@context) assert_equal 'Assert case Insensitive', Template.parse('{{words | sort_natural | join}}').render(@context)
# Test hashes # Test hashes
assert_equal 'A b C', Template.parse("{{hashes | sort_natural: 'a' | map: 'a' | join}}").render(@context) assert_equal 'A b C', Template.parse("{{hashes | sort_natural: 'a' | map: 'a' | join}}").render(@context)
@@ -104,7 +106,7 @@ class FiltersTest < Minitest::Test
@context['objects'] = [TestObject.new('A'), TestObject.new(nil), TestObject.new('C')] @context['objects'] = [TestObject.new('A'), TestObject.new(nil), TestObject.new('C')]
# Test strings # Test strings
assert_equal 'a b c', Template.parse("{{words | compact | join}}").render(@context) assert_equal 'a b c', Template.parse('{{words | compact | join}}').render(@context)
# Test hashes # Test hashes
assert_equal 'A C', Template.parse("{{hashes | compact: 'a' | map: 'a' | join}}").render(@context) assert_equal 'A C', Template.parse("{{hashes | compact: 'a' | map: 'a' | join}}").render(@context)
@@ -114,27 +116,27 @@ class FiltersTest < Minitest::Test
end end
def test_strip_html def test_strip_html
@context['var'] = "<b>bla blub</a>" @context['var'] = '<b>bla blub</a>'
assert_equal "bla blub", Template.parse("{{ var | strip_html }}").render(@context) assert_equal 'bla blub', Template.parse('{{ var | strip_html }}').render(@context)
end end
def test_strip_html_ignore_comments_with_html def test_strip_html_ignore_comments_with_html
@context['var'] = "<!-- split and some <ul> tag --><b>bla blub</a>" @context['var'] = '<!-- split and some <ul> tag --><b>bla blub</a>'
assert_equal "bla blub", Template.parse("{{ var | strip_html }}").render(@context) assert_equal 'bla blub', Template.parse('{{ var | strip_html }}').render(@context)
end end
def test_capitalize def test_capitalize
@context['var'] = "blub" @context['var'] = 'blub'
assert_equal "Blub", Template.parse("{{ var | capitalize }}").render(@context) assert_equal 'Blub', Template.parse('{{ var | capitalize }}').render(@context)
end end
def test_nonexistent_filter_is_ignored def test_nonexistent_filter_is_ignored
@context['var'] = 1000 @context['var'] = 1000
assert_equal '1000', Template.parse("{{ var | xyzzy }}").render(@context) assert_equal '1000', Template.parse('{{ var | xyzzy }}').render(@context)
end end
def test_filter_with_keyword_arguments def test_filter_with_keyword_arguments
@@ -146,10 +148,10 @@ class FiltersTest < Minitest::Test
end end
def test_override_object_method_in_filter def test_override_object_method_in_filter
assert_equal "tap overridden", Template.parse("{{var | tap}}").render!({ 'var' => 1000 }, filters: [OverrideObjectMethodFilter]) assert_equal 'tap overridden', Template.parse('{{var | tap}}').render!({ 'var' => 1000 }, filters: [OverrideObjectMethodFilter])
# tap still treated as a non-existent filter # tap still treated as a non-existent filter
assert_equal "1000", Template.parse("{{var | tap}}").render!({ 'var' => 1000 }) assert_equal '1000', Template.parse('{{var | tap}}').render!('var' => 1000)
end end
end end
@@ -158,15 +160,15 @@ class FiltersInTemplate < Minitest::Test
def test_local_global def test_local_global
with_global_filter(MoneyFilter) do with_global_filter(MoneyFilter) do
assert_equal " 1000$ ", Template.parse("{{1000 | money}}").render!(nil, nil) assert_equal ' 1000$ ', Template.parse('{{1000 | money}}').render!(nil, nil)
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, filters: CanadianMoneyFilter) assert_equal ' 1000$ CAD ', Template.parse('{{1000 | money}}').render!(nil, filters: CanadianMoneyFilter)
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, filters: [CanadianMoneyFilter]) assert_equal ' 1000$ CAD ', Template.parse('{{1000 | money}}').render!(nil, filters: [CanadianMoneyFilter])
end end
end end
def test_local_filter_with_deprecated_syntax def test_local_filter_with_deprecated_syntax
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, CanadianMoneyFilter) assert_equal ' 1000$ CAD ', Template.parse('{{1000 | money}}').render!(nil, CanadianMoneyFilter)
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, [CanadianMoneyFilter]) assert_equal ' 1000$ CAD ', Template.parse('{{1000 | money}}').render!(nil, [CanadianMoneyFilter])
end end
end # FiltersTest end # FiltersTest

View File

@@ -1,15 +1,17 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class HashOrderingTest < Minitest::Test class HashOrderingTest < Minitest::Test
module MoneyFilter module MoneyFilter
def money(input) def money(input)
sprintf(' %d$ ', input) format(' %d$ ', input)
end end
end end
module CanadianMoneyFilter module CanadianMoneyFilter
def money(input) def money(input)
sprintf(' %d$ CAD ', input) format(' %d$ CAD ', input)
end end
end end
@@ -17,7 +19,7 @@ class HashOrderingTest < Minitest::Test
def test_global_register_order def test_global_register_order
with_global_filter(MoneyFilter, CanadianMoneyFilter) do with_global_filter(MoneyFilter, CanadianMoneyFilter) do
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render(nil, nil) assert_equal ' 1000$ CAD ', Template.parse('{{1000 | money}}').render(nil, nil)
end end
end end
end end

View File

@@ -0,0 +1,13 @@
# frozen_string_literal: true
require 'test_helper'
require 'liquid/legacy'
class FiltersTest < Minitest::Test
include Liquid
def test_constants
assert_equal Liquid::FilterSeparator, Liquid::FILTER_SEPARATOR
assert_equal Liquid::BlockBody::ContentOfVariable, Liquid::BlockBody::CONTENT_OF_VARIABLE
end
end

View File

@@ -1,7 +1,9 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
module FunnyFilter module FunnyFilter
def make_funny(input) def make_funny(_input)
'LOL' 'LOL'
end end
@@ -9,11 +11,11 @@ module FunnyFilter
"LOL: #{input}" "LOL: #{input}"
end end
def add_smiley(input, smiley = ":-)") def add_smiley(input, smiley = ':-)')
"#{input} #{smiley}" "#{input} #{smiley}"
end end
def add_tag(input, tag = "p", id = "foo") def add_tag(input, tag = 'p', id = 'foo')
%(<#{tag} id="#{id}">#{input}</#{tag}>) %(<#{tag} id="#{id}">#{input}</#{tag}>)
end end
@@ -32,7 +34,7 @@ class OutputTest < Minitest::Test
def setup def setup
@assigns = { @assigns = {
'best_cars' => 'bmw', 'best_cars' => 'bmw',
'car' => { 'bmw' => 'good', 'gm' => 'bad' } 'car' => { 'bmw' => 'good', 'gm' => 'bad' },
} }
end end
@@ -45,9 +47,9 @@ class OutputTest < Minitest::Test
def test_variable_traversing_with_two_brackets def test_variable_traversing_with_two_brackets
text = %({{ site.data.menu[include.menu][include.locale] }}) text = %({{ site.data.menu[include.menu][include.locale] }})
assert_equal "it works!", Template.parse(text).render!( assert_equal 'it works!', Template.parse(text).render!(
"site" => { "data" => { "menu" => { "foo" => { "bar" => "it works!" } } } }, 'site' => { 'data' => { 'menu' => { 'foo' => { 'bar' => 'it works!' } } } },
"include" => { "menu" => "foo", "locale" => "bar" } 'include' => { 'menu' => 'foo', 'locale' => 'bar' }
) )
end end

View File

@@ -0,0 +1,247 @@
# frozen_string_literal: true
require 'test_helper'
class ParseTreeVisitorTest < Minitest::Test
include Liquid
def test_variable
assert_equal(
['test'],
visit(%({{ test }}))
)
end
def test_varible_with_filter
assert_equal(
%w[test infilter],
visit(%({{ test | split: infilter }}))
)
end
def test_dynamic_variable
assert_equal(
%w[test inlookup],
visit(%({{ test[inlookup] }}))
)
end
def test_if_condition
assert_equal(
['test'],
visit(%({% if test %}{% endif %}))
)
end
def test_complex_if_condition
assert_equal(
['test'],
visit(%({% if 1 == 1 and 2 == test %}{% endif %}))
)
end
def test_if_body
assert_equal(
['test'],
visit(%({% if 1 == 1 %}{{ test }}{% endif %}))
)
end
def test_unless_condition
assert_equal(
['test'],
visit(%({% unless test %}{% endunless %}))
)
end
def test_complex_unless_condition
assert_equal(
['test'],
visit(%({% unless 1 == 1 and 2 == test %}{% endunless %}))
)
end
def test_unless_body
assert_equal(
['test'],
visit(%({% unless 1 == 1 %}{{ test }}{% endunless %}))
)
end
def test_elsif_condition
assert_equal(
['test'],
visit(%({% if 1 == 1 %}{% elsif test %}{% endif %}))
)
end
def test_complex_elsif_condition
assert_equal(
['test'],
visit(%({% if 1 == 1 %}{% elsif 1 == 1 and 2 == test %}{% endif %}))
)
end
def test_elsif_body
assert_equal(
['test'],
visit(%({% if 1 == 1 %}{% elsif 2 == 2 %}{{ test }}{% endif %}))
)
end
def test_else_body
assert_equal(
['test'],
visit(%({% if 1 == 1 %}{% else %}{{ test }}{% endif %}))
)
end
def test_case_left
assert_equal(
['test'],
visit(%({% case test %}{% endcase %}))
)
end
def test_case_condition
assert_equal(
['test'],
visit(%({% case 1 %}{% when test %}{% endcase %}))
)
end
def test_case_when_body
assert_equal(
['test'],
visit(%({% case 1 %}{% when 2 %}{{ test }}{% endcase %}))
)
end
def test_case_else_body
assert_equal(
['test'],
visit(%({% case 1 %}{% else %}{{ test }}{% endcase %}))
)
end
def test_for_in
assert_equal(
['test'],
visit(%({% for x in test %}{% endfor %}))
)
end
def test_for_limit
assert_equal(
['test'],
visit(%({% for x in (1..5) limit: test %}{% endfor %}))
)
end
def test_for_offset
assert_equal(
['test'],
visit(%({% for x in (1..5) offset: test %}{% endfor %}))
)
end
def test_for_body
assert_equal(
['test'],
visit(%({% for x in (1..5) %}{{ test }}{% endfor %}))
)
end
def test_tablerow_in
assert_equal(
['test'],
visit(%({% tablerow x in test %}{% endtablerow %}))
)
end
def test_tablerow_limit
assert_equal(
['test'],
visit(%({% tablerow x in (1..5) limit: test %}{% endtablerow %}))
)
end
def test_tablerow_offset
assert_equal(
['test'],
visit(%({% tablerow x in (1..5) offset: test %}{% endtablerow %}))
)
end
def test_tablerow_body
assert_equal(
['test'],
visit(%({% tablerow x in (1..5) %}{{ test }}{% endtablerow %}))
)
end
def test_cycle
assert_equal(
['test'],
visit(%({% cycle test %}))
)
end
def test_assign
assert_equal(
['test'],
visit(%({% assign x = test %}))
)
end
def test_capture
assert_equal(
['test'],
visit(%({% capture x %}{{ test }}{% endcapture %}))
)
end
def test_include
assert_equal(
['test'],
visit(%({% include test %}))
)
end
def test_include_with
assert_equal(
['test'],
visit(%({% include "hai" with test %}))
)
end
def test_include_for
assert_equal(
['test'],
visit(%({% include "hai" for test %}))
)
end
def test_preserve_tree_structure
assert_equal(
[[nil, [
[nil, [[nil, [['other', []]]]]],
['test', []],
['xs', []],
]]],
traversal(%({% for x in xs offset: test %}{{ other }}{% endfor %})).visit
)
end
private
def traversal(template)
ParseTreeVisitor
.for(Template.parse(template).root)
.add_callback_for(VariableLookup, &:name)
end
def visit(template)
traversal(template).visit.flatten.compact
end
end

View File

@@ -1,41 +1,43 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class ParsingQuirksTest < Minitest::Test class ParsingQuirksTest < Minitest::Test
include Liquid include Liquid
def test_parsing_css def test_parsing_css
text = " div { font-weight: bold; } " text = ' div { font-weight: bold; } '
assert_equal text, Template.parse(text).render! assert_equal text, Template.parse(text).render!
end end
def test_raise_on_single_close_bracet def test_raise_on_single_close_bracet
assert_raises(SyntaxError) do assert_raises(SyntaxError) do
Template.parse("text {{method} oh nos!") Template.parse('text {{method} oh nos!')
end end
end end
def test_raise_on_label_and_no_close_bracets def test_raise_on_label_and_no_close_bracets
assert_raises(SyntaxError) do assert_raises(SyntaxError) do
Template.parse("TEST {{ ") Template.parse('TEST {{ ')
end end
end end
def test_raise_on_label_and_no_close_bracets_percent def test_raise_on_label_and_no_close_bracets_percent
assert_raises(SyntaxError) do assert_raises(SyntaxError) do
Template.parse("TEST {% ") Template.parse('TEST {% ')
end end
end end
def test_error_on_empty_filter def test_error_on_empty_filter
assert Template.parse("{{test}}") assert Template.parse('{{test}}')
with_error_mode(:lax) do with_error_mode(:lax) do
assert Template.parse("{{|test}}") assert Template.parse('{{|test}}')
end end
with_error_mode(:strict) do with_error_mode(:strict) do
assert_raises(SyntaxError) { Template.parse("{{|test}}") } assert_raises(SyntaxError) { Template.parse('{{|test}}') }
assert_raises(SyntaxError) { Template.parse("{{test |a|b|}}") } assert_raises(SyntaxError) { Template.parse('{{test |a|b|}}') }
end end
end end
@@ -51,20 +53,20 @@ class ParsingQuirksTest < Minitest::Test
def test_unexpected_characters_syntax_error def test_unexpected_characters_syntax_error
with_error_mode(:strict) do with_error_mode(:strict) do
assert_raises(SyntaxError) do assert_raises(SyntaxError) do
markup = "true && false" markup = 'true && false'
Template.parse("{% if #{markup} %} YES {% endif %}") Template.parse("{% if #{markup} %} YES {% endif %}")
end end
assert_raises(SyntaxError) do assert_raises(SyntaxError) do
markup = "false || true" markup = 'false || true'
Template.parse("{% if #{markup} %} YES {% endif %}") Template.parse("{% if #{markup} %} YES {% endif %}")
end end
end end
end end
def test_no_error_on_lax_empty_filter def test_no_error_on_lax_empty_filter
assert Template.parse("{{test |a|b|}}", error_mode: :lax) assert Template.parse('{{test |a|b|}}', error_mode: :lax)
assert Template.parse("{{test}}", error_mode: :lax) assert Template.parse('{{test}}', error_mode: :lax)
assert Template.parse("{{|test|}}", error_mode: :lax) assert Template.parse('{{|test|}}', error_mode: :lax)
end end
def test_meaningless_parens_lax def test_meaningless_parens_lax
@@ -77,9 +79,9 @@ class ParsingQuirksTest < Minitest::Test
def test_unexpected_characters_silently_eat_logic_lax def test_unexpected_characters_silently_eat_logic_lax
with_error_mode(:lax) do with_error_mode(:lax) do
markup = "true && false" markup = 'true && false'
assert_template_result(' YES ', "{% if #{markup} %} YES {% endif %}") assert_template_result(' YES ', "{% if #{markup} %} YES {% endif %}")
markup = "false || true" markup = 'false || true'
assert_template_result('', "{% if #{markup} %} YES {% endif %}") assert_template_result('', "{% if #{markup} %} YES {% endif %}")
end end
end end
@@ -99,7 +101,7 @@ class ParsingQuirksTest < Minitest::Test
# After the messed up quotes a filter without parameters (reverse) should work # After the messed up quotes a filter without parameters (reverse) should work
# but one with parameters (remove) shouldn't be detected. # but one with parameters (remove) shouldn't be detected.
assert_template_result('here', "{{ 'hi there' | split:\"t\"\" | reverse | first}}") assert_template_result('here', "{{ 'hi there' | split:\"t\"\" | reverse | first}}")
assert_template_result('hi ', "{{ 'hi there' | split:\"t\"\" | remove:\"i\" | first}}") assert_template_result('hi ', "{{ 'hi there' | split:\"t\"\" | remove:\"i\" | first}}")
end end
end end
@@ -112,7 +114,7 @@ class ParsingQuirksTest < Minitest::Test
def test_extra_dots_in_ranges def test_extra_dots_in_ranges
with_error_mode(:lax) do with_error_mode(:lax) do
assert_template_result('12345', "{% for i in (1...5) %}{{ i }}{% endfor %}") assert_template_result('12345', '{% for i in (1...5) %}{{ i }}{% endfor %}')
end end
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class RenderProfilingTest < Minitest::Test class RenderProfilingTest < Minitest::Test
@@ -72,7 +74,7 @@ class RenderProfilingTest < Minitest::Test
t = Template.parse("{% include 'a_template' %}", profile: true) t = Template.parse("{% include 'a_template' %}", profile: true)
t.render! t.render!
assert t.profiler.total_render_time >= 0, "Total render time was not calculated" assert t.profiler.total_render_time >= 0, 'Total render time was not calculated'
end end
def test_profiling_uses_include_to_mark_children def test_profiling_uses_include_to_mark_children
@@ -89,7 +91,7 @@ class RenderProfilingTest < Minitest::Test
include_node = t.profiler[1] include_node = t.profiler[1]
include_node.children.each do |child| include_node.children.each do |child|
assert_equal "a_template", child.partial assert_equal 'a_template', child.partial
end end
end end
@@ -99,12 +101,12 @@ class RenderProfilingTest < Minitest::Test
a_template = t.profiler[1] a_template = t.profiler[1]
a_template.children.each do |child| a_template.children.each do |child|
assert_equal "a_template", child.partial assert_equal 'a_template', child.partial
end end
b_template = t.profiler[2] b_template = t.profiler[2]
b_template.children.each do |child| b_template.children.each do |child|
assert_equal "b_template", child.partial assert_equal 'b_template', child.partial
end end
end end
@@ -114,12 +116,12 @@ class RenderProfilingTest < Minitest::Test
a_template1 = t.profiler[1] a_template1 = t.profiler[1]
a_template1.children.each do |child| a_template1.children.each do |child|
assert_equal "a_template", child.partial assert_equal 'a_template', child.partial
end end
a_template2 = t.profiler[2] a_template2 = t.profiler[2]
a_template2.children.each do |child| a_template2.children.each do |child|
assert_equal "a_template", child.partial assert_equal 'a_template', child.partial
end end
end end
@@ -128,7 +130,7 @@ class RenderProfilingTest < Minitest::Test
t.render! t.render!
timing_count = 0 timing_count = 0
t.profiler.each do |timing| t.profiler.each do |_timing|
timing_count += 1 timing_count += 1
end end
@@ -136,7 +138,7 @@ class RenderProfilingTest < Minitest::Test
end end
def test_profiling_marks_children_of_if_blocks def test_profiling_marks_children_of_if_blocks
t = Template.parse("{% if true %} {% increment test %} {{ test }} {% endif %}", profile: true) t = Template.parse('{% if true %} {% increment test %} {{ test }} {% endif %}', profile: true)
t.render! t.render!
assert_equal 1, t.profiler.length assert_equal 1, t.profiler.length
@@ -144,8 +146,8 @@ class RenderProfilingTest < Minitest::Test
end end
def test_profiling_marks_children_of_for_blocks def test_profiling_marks_children_of_for_blocks
t = Template.parse("{% for item in collection %} {{ item }} {% endfor %}", profile: true) t = Template.parse('{% for item in collection %} {{ item }} {% endfor %}', profile: true)
t.render!({ "collection" => ["one", "two"] }) t.render!('collection' => %w[one two])
assert_equal 1, t.profiler.length assert_equal 1, t.profiler.length
# Will profile each invocation of the for block # Will profile each invocation of the for block

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
module SecurityFilter module SecurityFilter
@@ -57,22 +59,22 @@ class SecurityTest < Minitest::Test
current_symbols = Symbol.all_symbols current_symbols = Symbol.all_symbols
assigns = { 'drop' => Drop.new } assigns = { 'drop' => Drop.new }
assert_equal "", Template.parse("{{ drop.custom_method_1 }}", assigns).render! assert_equal '', Template.parse('{{ drop.custom_method_1 }}', assigns).render!
assert_equal "", Template.parse("{{ drop.custom_method_2 }}", assigns).render! assert_equal '', Template.parse('{{ drop.custom_method_2 }}', assigns).render!
assert_equal "", Template.parse("{{ drop.custom_method_3 }}", assigns).render! assert_equal '', Template.parse('{{ drop.custom_method_3 }}', assigns).render!
assert_equal [], (Symbol.all_symbols - current_symbols) assert_equal [], (Symbol.all_symbols - current_symbols)
end end
def test_max_depth_nested_blocks_does_not_raise_exception def test_max_depth_nested_blocks_does_not_raise_exception
depth = Liquid::Block::MAX_DEPTH depth = Liquid::Block::MAX_DEPTH
code = "{% if true %}" * depth + "rendered" + "{% endif %}" * depth code = '{% if true %}' * depth + 'rendered' + '{% endif %}' * depth
assert_equal "rendered", Template.parse(code).render! assert_equal 'rendered', Template.parse(code).render!
end end
def test_more_than_max_depth_nested_blocks_raises_exception def test_more_than_max_depth_nested_blocks_raises_exception
depth = Liquid::Block::MAX_DEPTH + 1 depth = Liquid::Block::MAX_DEPTH + 1
code = "{% if true %}" * depth + "rendered" + "{% endif %}" * depth code = '{% if true %}' * depth + 'rendered' + '{% endif %}' * depth
assert_raises(Liquid::StackLevelError) do assert_raises(Liquid::StackLevelError) do
Template.parse(code).render! Template.parse(code).render!
end end

View File

@@ -1,4 +1,4 @@
# encoding: utf-8 # frozen_string_literal: true
require 'test_helper' require 'test_helper'
@@ -17,7 +17,7 @@ class TestThing
"woot: #{@foo}" "woot: #{@foo}"
end end
def [](whatever) def [](_whatever)
to_s to_s
end end
@@ -29,7 +29,7 @@ end
class TestDrop < Liquid::Drop class TestDrop < Liquid::Drop
def test def test
"testfoo" 'testfoo'
end end
end end
@@ -37,7 +37,7 @@ class TestEnumerable < Liquid::Drop
include Enumerable include Enumerable
def each(&block) def each(&block)
[ { "foo" => 1, "bar" => 2 }, { "foo" => 2, "bar" => 1 }, { "foo" => 3, "bar" => 3 } ].each(&block) [{ 'foo' => 1, 'bar' => 2 }, { 'foo' => 2, 'bar' => 1 }, { 'foo' => 3, 'bar' => 3 }].each(&block)
end end
end end
@@ -65,12 +65,12 @@ class StandardFiltersTest < Minitest::Test
end end
def test_downcase def test_downcase
assert_equal 'testing', @filters.downcase("Testing") assert_equal 'testing', @filters.downcase('Testing')
assert_equal '', @filters.downcase(nil) assert_equal '', @filters.downcase(nil)
end end
def test_upcase def test_upcase
assert_equal 'TESTING', @filters.upcase("Testing") assert_equal 'TESTING', @filters.upcase('Testing')
assert_equal '', @filters.upcase(nil) assert_equal '', @filters.upcase(nil)
end end
@@ -91,22 +91,22 @@ class StandardFiltersTest < Minitest::Test
@filters.slice('foobar', nil) @filters.slice('foobar', nil)
end end
assert_raises(Liquid::ArgumentError) do assert_raises(Liquid::ArgumentError) do
@filters.slice('foobar', 0, "") @filters.slice('foobar', 0, '')
end end
end end
def test_slice_on_arrays def test_slice_on_arrays
input = 'foobar'.split(//) input = 'foobar'.split(//)
assert_equal %w(o o b), @filters.slice(input, 1, 3) assert_equal %w[o o b], @filters.slice(input, 1, 3)
assert_equal %w(o o b a r), @filters.slice(input, 1, 1000) assert_equal %w[o o b a r], @filters.slice(input, 1, 1000)
assert_equal %w(), @filters.slice(input, 1, 0) assert_equal %w[], @filters.slice(input, 1, 0)
assert_equal %w(o), @filters.slice(input, 1, 1) assert_equal %w[o], @filters.slice(input, 1, 1)
assert_equal %w(b a r), @filters.slice(input, 3, 3) assert_equal %w[b a r], @filters.slice(input, 3, 3)
assert_equal %w(a r), @filters.slice(input, -2, 2) assert_equal %w[a r], @filters.slice(input, -2, 2)
assert_equal %w(a r), @filters.slice(input, -2, 1000) assert_equal %w[a r], @filters.slice(input, -2, 1000)
assert_equal %w(r), @filters.slice(input, -1) assert_equal %w[r], @filters.slice(input, -1)
assert_equal %w(), @filters.slice(input, 100, 10) assert_equal %w[], @filters.slice(input, 100, 10)
assert_equal %w(), @filters.slice(input, -100, 10) assert_equal %w[], @filters.slice(input, -100, 10)
end end
def test_truncate def test_truncate
@@ -114,16 +114,16 @@ class StandardFiltersTest < Minitest::Test
assert_equal '1234567890', @filters.truncate('1234567890', 20) assert_equal '1234567890', @filters.truncate('1234567890', 20)
assert_equal '...', @filters.truncate('1234567890', 0) assert_equal '...', @filters.truncate('1234567890', 0)
assert_equal '1234567890', @filters.truncate('1234567890') assert_equal '1234567890', @filters.truncate('1234567890')
assert_equal "测试...", @filters.truncate("测试测试测试测试", 5) assert_equal '测试...', @filters.truncate('测试测试测试测试', 5)
assert_equal '12341', @filters.truncate("1234567890", 5, 1) assert_equal '12341', @filters.truncate('1234567890', 5, 1)
end end
def test_split def test_split
assert_equal ['12', '34'], @filters.split('12~34', '~') assert_equal %w[12 34], @filters.split('12~34', '~')
assert_equal ['A? ', ' ,Z'], @filters.split('A? ~ ~ ~ ,Z', '~ ~ ~') assert_equal ['A? ', ' ,Z'], @filters.split('A? ~ ~ ~ ,Z', '~ ~ ~')
assert_equal ['A?Z'], @filters.split('A?Z', '~') assert_equal ['A?Z'], @filters.split('A?Z', '~')
assert_equal [], @filters.split(nil, ' ') assert_equal [], @filters.split(nil, ' ')
assert_equal ['A', 'Z'], @filters.split('A1Z', 1) assert_equal %w[A Z], @filters.split('A1Z', 1)
end end
def test_escape def test_escape
@@ -158,6 +158,10 @@ class StandardFiltersTest < Minitest::Test
assert_equal '1', @filters.url_decode(1) assert_equal '1', @filters.url_decode(1)
assert_equal '2001-02-03', @filters.url_decode(Date.new(2001, 2, 3)) assert_equal '2001-02-03', @filters.url_decode(Date.new(2001, 2, 3))
assert_nil @filters.url_decode(nil) assert_nil @filters.url_decode(nil)
exception = assert_raises Liquid::ArgumentError do
@filters.url_decode('%ff')
end
assert_equal 'Liquid error: invalid byte sequence in UTF-8', exception.message
end end
def test_truncatewords def test_truncatewords
@@ -165,18 +169,21 @@ class StandardFiltersTest < Minitest::Test
assert_equal 'one two...', @filters.truncatewords('one two three', 2) assert_equal 'one two...', @filters.truncatewords('one two three', 2)
assert_equal 'one two three', @filters.truncatewords('one two three') assert_equal 'one two three', @filters.truncatewords('one two three')
assert_equal 'Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221;...', @filters.truncatewords('Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221; x 16&#8221; x 10.5&#8221; high) with cover.', 15) assert_equal 'Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221;...', @filters.truncatewords('Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221; x 16&#8221; x 10.5&#8221; high) with cover.', 15)
assert_equal "测试测试测试测试", @filters.truncatewords('测试测试测试测试', 5) assert_equal '测试测试测试测试', @filters.truncatewords('测试测试测试测试', 5)
assert_equal 'one two1', @filters.truncatewords("one two three", 2, 1) assert_equal 'one two1', @filters.truncatewords('one two three', 2, 1)
end end
def test_strip_html def test_strip_html
assert_equal 'test', @filters.strip_html("<div>test</div>") assert_equal 'test', @filters.strip_html('<div>test</div>')
assert_equal 'test', @filters.strip_html("<div id='test'>test</div>") assert_equal 'test', @filters.strip_html("<div id='test'>test</div>")
assert_equal '', @filters.strip_html("<script type='text/javascript'>document.write('some stuff');</script>") assert_equal '', @filters.strip_html("<script type='text/javascript'>document.write('some stuff');</script>")
assert_equal '', @filters.strip_html("<style type='text/css'>foo bar</style>") assert_equal '', @filters.strip_html("<style type='text/css'>foo bar</style>")
assert_equal 'test', @filters.strip_html("<div\nclass='multiline'>test</div>") assert_equal 'test', @filters.strip_html("<div\nclass='multiline'>test</div>")
assert_equal 'test', @filters.strip_html("<!-- foo bar \n test -->test") assert_equal 'test', @filters.strip_html("<!-- foo bar \n test -->test")
assert_equal '', @filters.strip_html(nil) assert_equal '', @filters.strip_html(nil)
# Quirk of the existing implementation
assert_equal 'foo;', @filters.strip_html('<<<script </script>script>foo;</script>')
end end
def test_join def test_join
@@ -187,60 +194,164 @@ class StandardFiltersTest < Minitest::Test
def test_sort def test_sort
assert_equal [1, 2, 3, 4], @filters.sort([4, 3, 2, 1]) 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") assert_equal [{ 'a' => 1 }, { 'a' => 2 }, { 'a' => 3 }, { 'a' => 4 }], @filters.sort([{ 'a' => 4 }, { 'a' => 3 }, { 'a' => 1 }, { 'a' => 2 }], 'a')
end
def test_sort_with_nils
assert_equal [1, 2, 3, 4, nil], @filters.sort([nil, 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')
end end
def test_sort_when_property_is_sometimes_missing_puts_nils_last def test_sort_when_property_is_sometimes_missing_puts_nils_last
input = [ input = [
{ "price" => 4, "handle" => "alpha" }, { 'price' => 4, 'handle' => 'alpha' },
{ "handle" => "beta" }, { 'handle' => 'beta' },
{ "price" => 1, "handle" => "gamma" }, { 'price' => 1, 'handle' => 'gamma' },
{ "handle" => "delta" }, { 'handle' => 'delta' },
{ "price" => 2, "handle" => "epsilon" } { 'price' => 2, 'handle' => 'epsilon' },
] ]
expectation = [ expectation = [
{ "price" => 1, "handle" => "gamma" }, { 'price' => 1, 'handle' => 'gamma' },
{ "price" => 2, "handle" => "epsilon" }, { 'price' => 2, 'handle' => 'epsilon' },
{ "price" => 4, "handle" => "alpha" }, { 'price' => 4, 'handle' => 'alpha' },
{ "handle" => "delta" }, { 'handle' => 'delta' },
{ "handle" => "beta" } { 'handle' => 'beta' },
] ]
assert_equal expectation, @filters.sort(input, "price") assert_equal expectation, @filters.sort(input, 'price')
end
def test_sort_natural
assert_equal %w[a B c D], @filters.sort_natural(%w[c D a B])
assert_equal [{ 'a' => 'a' }, { 'a' => 'B' }, { 'a' => 'c' }, { 'a' => 'D' }], @filters.sort_natural([{ 'a' => 'D' }, { 'a' => 'c' }, { 'a' => 'a' }, { 'a' => 'B' }], 'a')
end
def test_sort_natural_with_nils
assert_equal ['a', 'B', 'c', 'D', nil], @filters.sort_natural([nil, 'c', 'D', 'a', 'B'])
assert_equal [{ 'a' => 'a' }, { 'a' => 'B' }, { 'a' => 'c' }, { 'a' => 'D' }, {}], @filters.sort_natural([{ 'a' => 'D' }, { 'a' => 'c' }, {}, { 'a' => 'a' }, { 'a' => 'B' }], 'a')
end
def test_sort_natural_when_property_is_sometimes_missing_puts_nils_last
input = [
{ 'price' => '4', 'handle' => 'alpha' },
{ 'handle' => 'beta' },
{ 'price' => '1', 'handle' => 'gamma' },
{ 'handle' => 'delta' },
{ 'price' => 2, 'handle' => 'epsilon' },
]
expectation = [
{ 'price' => '1', 'handle' => 'gamma' },
{ 'price' => 2, 'handle' => 'epsilon' },
{ 'price' => '4', 'handle' => 'alpha' },
{ 'handle' => 'delta' },
{ 'handle' => 'beta' },
]
assert_equal expectation, @filters.sort_natural(input, 'price')
end
def test_sort_natural_case_check
input = [
{ 'key' => 'X' },
{ 'key' => 'Y' },
{ 'key' => 'Z' },
{ 'fake' => 't' },
{ 'key' => 'a' },
{ 'key' => 'b' },
{ 'key' => 'c' },
]
expectation = [
{ 'key' => 'a' },
{ 'key' => 'b' },
{ 'key' => 'c' },
{ 'key' => 'X' },
{ 'key' => 'Y' },
{ 'key' => 'Z' },
{ 'fake' => 't' },
]
assert_equal expectation, @filters.sort_natural(input, 'key')
assert_equal %w[a b c X Y Z], @filters.sort_natural(%w[X Y Z a b c])
end end
def test_sort_empty_array def test_sort_empty_array
assert_equal [], @filters.sort([], "a") assert_equal [], @filters.sort([], 'a')
end
def test_sort_invalid_property
foo = [
[1],
[2],
[3],
]
assert_raises Liquid::ArgumentError do
@filters.sort(foo, 'bar')
end
end end
def test_sort_natural_empty_array def test_sort_natural_empty_array
assert_equal [], @filters.sort_natural([], "a") assert_equal [], @filters.sort_natural([], 'a')
end
def test_sort_natural_invalid_property
foo = [
[1],
[2],
[3],
]
assert_raises Liquid::ArgumentError do
@filters.sort_natural(foo, 'bar')
end
end end
def test_legacy_sort_hash def test_legacy_sort_hash
assert_equal [{ a: 1, b: 2 }], @filters.sort({ a: 1, b: 2 }) assert_equal [{ a: 1, b: 2 }], @filters.sort(a: 1, b: 2)
end end
def test_numerical_vs_lexicographical_sort def test_numerical_vs_lexicographical_sort
assert_equal [2, 10], @filters.sort([10, 2]) assert_equal [2, 10], @filters.sort([10, 2])
assert_equal [{ "a" => 2 }, { "a" => 10 }], @filters.sort([{ "a" => 10 }, { "a" => 2 }], "a") assert_equal [{ 'a' => 2 }, { 'a' => 10 }], @filters.sort([{ 'a' => 10 }, { 'a' => 2 }], 'a')
assert_equal ["10", "2"], @filters.sort(["10", "2"]) assert_equal %w[10 2], @filters.sort(%w[10 2])
assert_equal [{ "a" => "10" }, { "a" => "2" }], @filters.sort([{ "a" => "10" }, { "a" => "2" }], "a") assert_equal [{ 'a' => '10' }, { 'a' => '2' }], @filters.sort([{ 'a' => '10' }, { 'a' => '2' }], 'a')
end end
def test_uniq def test_uniq
assert_equal ["foo"], @filters.uniq("foo") assert_equal ['foo'], @filters.uniq('foo')
assert_equal [1, 3, 2, 4], @filters.uniq([1, 1, 3, 2, 3, 1, 4, 3, 2, 1]) assert_equal [1, 3, 2, 4], @filters.uniq([1, 1, 3, 2, 3, 1, 4, 3, 2, 1])
assert_equal [{ "a" => 1 }, { "a" => 3 }, { "a" => 2 }], @filters.uniq([{ "a" => 1 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a") assert_equal [{ 'a' => 1 }, { 'a' => 3 }, { 'a' => 2 }], @filters.uniq([{ 'a' => 1 }, { 'a' => 3 }, { 'a' => 1 }, { 'a' => 2 }], 'a')
testdrop = TestDrop.new testdrop = TestDrop.new
assert_equal [testdrop], @filters.uniq([testdrop, TestDrop.new], 'test') assert_equal [testdrop], @filters.uniq([testdrop, TestDrop.new], 'test')
end end
def test_uniq_empty_array def test_uniq_empty_array
assert_equal [], @filters.uniq([], "a") assert_equal [], @filters.uniq([], 'a')
end
def test_uniq_invalid_property
foo = [
[1],
[2],
[3],
]
assert_raises Liquid::ArgumentError do
@filters.uniq(foo, 'bar')
end
end end
def test_compact_empty_array def test_compact_empty_array
assert_equal [], @filters.compact([], "a") assert_equal [], @filters.compact([], 'a')
end
def test_compact_invalid_property
foo = [
[1],
[2],
[3],
]
assert_raises Liquid::ArgumentError do
@filters.compact(foo, 'bar')
end
end end
def test_reverse def test_reverse
@@ -252,64 +363,87 @@ class StandardFiltersTest < Minitest::Test
end end
def test_map def test_map
assert_equal [1, 2, 3, 4], @filters.map([{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }], 'a') assert_equal [1, 2, 3, 4], @filters.map([{ 'a' => 1 }, { 'a' => 2 }, { 'a' => 3 }, { 'a' => 4 }], 'a')
assert_template_result 'abc', "{{ ary | map:'foo' | map:'bar' }}", assert_template_result 'abc', "{{ ary | map:'foo' | map:'bar' }}",
'ary' => [{ 'foo' => { 'bar' => 'a' } }, { 'foo' => { 'bar' => 'b' } }, { 'foo' => { 'bar' => 'c' } }] 'ary' => [{ 'foo' => { 'bar' => 'a' } }, { 'foo' => { 'bar' => 'b' } }, { 'foo' => { 'bar' => 'c' } }]
end end
def test_map_doesnt_call_arbitrary_stuff def test_map_doesnt_call_arbitrary_stuff
assert_template_result "", '{{ "foo" | map: "__id__" }}' assert_template_result '', '{{ "foo" | map: "__id__" }}'
assert_template_result "", '{{ "foo" | map: "inspect" }}' assert_template_result '', '{{ "foo" | map: "inspect" }}'
end end
def test_map_calls_to_liquid def test_map_calls_to_liquid
t = TestThing.new t = TestThing.new
assert_template_result "woot: 1", '{{ foo | map: "whatever" }}', "foo" => [t] assert_template_result 'woot: 1', '{{ foo | map: "whatever" }}', 'foo' => [t]
end end
def test_map_on_hashes def test_map_on_hashes
assert_template_result "4217", '{{ thing | map: "foo" | map: "bar" }}', assert_template_result '4217', '{{ thing | map: "foo" | map: "bar" }}',
"thing" => { "foo" => [ { "bar" => 42 }, { "bar" => 17 } ] } 'thing' => { 'foo' => [{ 'bar' => 42 }, { 'bar' => 17 }] }
end end
def test_legacy_map_on_hashes_with_dynamic_key def test_legacy_map_on_hashes_with_dynamic_key
template = "{% assign key = 'foo' %}{{ thing | map: key | map: 'bar' }}" template = "{% assign key = 'foo' %}{{ thing | map: key | map: 'bar' }}"
hash = { "foo" => { "bar" => 42 } } hash = { 'foo' => { 'bar' => 42 } }
assert_template_result "42", template, "thing" => hash assert_template_result '42', template, 'thing' => hash
end end
def test_sort_calls_to_liquid def test_sort_calls_to_liquid
t = TestThing.new t = TestThing.new
Liquid::Template.parse('{{ foo | sort: "whatever" }}').render("foo" => [t]) Liquid::Template.parse('{{ foo | sort: "whatever" }}').render('foo' => [t])
assert t.foo > 0 assert t.foo.positive?
end end
def test_map_over_proc def test_map_over_proc
drop = TestDrop.new drop = TestDrop.new
p = proc{ drop } p = proc { drop }
templ = '{{ procs | map: "test" }}' templ = '{{ procs | map: "test" }}'
assert_template_result "testfoo", templ, "procs" => [p] assert_template_result 'testfoo', templ, 'procs' => [p]
end end
def test_map_over_drops_returning_procs def test_map_over_drops_returning_procs
drops = [ drops = [
{ {
"proc" => ->{ "foo" }, 'proc' => -> { 'foo' },
}, },
{ {
"proc" => ->{ "bar" }, 'proc' => -> { 'bar' },
}, },
] ]
templ = '{{ drops | map: "proc" }}' templ = '{{ drops | map: "proc" }}'
assert_template_result "foobar", templ, "drops" => drops assert_template_result 'foobar', templ, 'drops' => drops
end end
def test_map_works_on_enumerables def test_map_works_on_enumerables
assert_template_result "123", '{{ foo | map: "foo" }}', "foo" => TestEnumerable.new assert_template_result '123', '{{ foo | map: "foo" }}', 'foo' => TestEnumerable.new
end
def test_map_returns_empty_on_2d_input_array
foo = [
[1],
[2],
[3],
]
assert_raises Liquid::ArgumentError do
@filters.map(foo, 'bar')
end
end
def test_map_returns_empty_with_no_property
foo = [
[1],
[2],
[3],
]
assert_raises Liquid::ArgumentError do
@filters.map(foo, nil)
end
end end
def test_sort_works_on_enumerables def test_sort_works_on_enumerables
assert_template_result "213", '{{ foo | sort: "bar" | map: "foo" }}', "foo" => TestEnumerable.new assert_template_result '213', '{{ foo | sort: "bar" | map: "foo" }}', 'foo' => TestEnumerable.new
end end
def test_first_and_last_call_to_liquid def test_first_and_last_call_to_liquid
@@ -318,37 +452,37 @@ class StandardFiltersTest < Minitest::Test
end end
def test_truncate_calls_to_liquid def test_truncate_calls_to_liquid
assert_template_result "wo...", '{{ foo | truncate: 5 }}', "foo" => TestThing.new assert_template_result 'wo...', '{{ foo | truncate: 5 }}', 'foo' => TestThing.new
end end
def test_date def test_date
assert_equal 'May', @filters.date(Time.parse("2006-05-05 10:00:00"), "%B") assert_equal 'May', @filters.date(Time.parse('2006-05-05 10:00:00'), '%B')
assert_equal 'June', @filters.date(Time.parse("2006-06-05 10:00:00"), "%B") assert_equal 'June', @filters.date(Time.parse('2006-06-05 10:00:00'), '%B')
assert_equal 'July', @filters.date(Time.parse("2006-07-05 10:00:00"), "%B") assert_equal 'July', @filters.date(Time.parse('2006-07-05 10:00:00'), '%B')
assert_equal 'May', @filters.date("2006-05-05 10:00:00", "%B") assert_equal 'May', @filters.date('2006-05-05 10:00:00', '%B')
assert_equal 'June', @filters.date("2006-06-05 10:00:00", "%B") assert_equal 'June', @filters.date('2006-06-05 10:00:00', '%B')
assert_equal 'July', @filters.date("2006-07-05 10:00:00", "%B") assert_equal 'July', @filters.date('2006-07-05 10:00:00', '%B')
assert_equal '2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", "") assert_equal '2006-07-05 10:00:00', @filters.date('2006-07-05 10:00:00', '')
assert_equal '2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", "") assert_equal '2006-07-05 10:00:00', @filters.date('2006-07-05 10:00:00', '')
assert_equal '2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", "") assert_equal '2006-07-05 10:00:00', @filters.date('2006-07-05 10:00:00', '')
assert_equal '2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", nil) assert_equal '2006-07-05 10:00:00', @filters.date('2006-07-05 10:00:00', nil)
assert_equal '07/05/2006', @filters.date("2006-07-05 10:00:00", "%m/%d/%Y") assert_equal '07/05/2006', @filters.date('2006-07-05 10:00:00', '%m/%d/%Y')
assert_equal "07/16/2004", @filters.date("Fri Jul 16 01:00:00 2004", "%m/%d/%Y") assert_equal '07/16/2004', @filters.date('Fri Jul 16 01:00:00 2004', '%m/%d/%Y')
assert_equal "#{Date.today.year}", @filters.date('now', '%Y') assert_equal Date.today.year.to_s, @filters.date('now', '%Y')
assert_equal "#{Date.today.year}", @filters.date('today', '%Y') assert_equal Date.today.year.to_s, @filters.date('today', '%Y')
assert_equal "#{Date.today.year}", @filters.date('Today', '%Y') assert_equal Date.today.year.to_s, @filters.date('Today', '%Y')
assert_nil @filters.date(nil, "%B") assert_nil @filters.date(nil, '%B')
assert_equal '', @filters.date('', "%B") assert_equal '', @filters.date('', '%B')
with_timezone("UTC") do with_timezone('UTC') do
assert_equal "07/05/2006", @filters.date(1152098955, "%m/%d/%Y") assert_equal '07/05/2006', @filters.date(1_152_098_955, '%m/%d/%Y')
assert_equal "07/05/2006", @filters.date("1152098955", "%m/%d/%Y") assert_equal '07/05/2006', @filters.date('1152098955', '%m/%d/%Y')
end end
end end
@@ -368,10 +502,10 @@ class StandardFiltersTest < Minitest::Test
end end
def test_remove def test_remove
assert_equal ' ', @filters.remove("a a a a", 'a') assert_equal ' ', @filters.remove('a a a a', 'a')
assert_equal ' ', @filters.remove("1 1 1 1", 1) assert_equal ' ', @filters.remove('1 1 1 1', 1)
assert_equal 'a a a', @filters.remove_first("a a a a", 'a ') assert_equal 'a a a', @filters.remove_first('a a a a', 'a ')
assert_equal ' 1 1 1', @filters.remove_first("1 1 1 1", 1) assert_equal ' 1 1 1', @filters.remove_first('1 1 1 1', 1)
assert_template_result 'a a a', "{{ 'a a a a' | remove_first: 'a ' }}" assert_template_result 'a a a', "{{ 'a a a a' | remove_first: 'a ' }}"
end end
@@ -380,126 +514,148 @@ class StandardFiltersTest < Minitest::Test
end end
def test_strip def test_strip
assert_template_result 'ab c', "{{ source | strip }}", 'source' => " ab c " assert_template_result 'ab c', '{{ source | strip }}', 'source' => ' ab c '
assert_template_result 'ab c', "{{ source | strip }}", 'source' => " \tab c \n \t" assert_template_result 'ab c', '{{ source | strip }}', 'source' => " \tab c \n \t"
end end
def test_lstrip def test_lstrip
assert_template_result 'ab c ', "{{ source | lstrip }}", 'source' => " ab c " assert_template_result 'ab c ', '{{ source | lstrip }}', 'source' => ' ab c '
assert_template_result "ab c \n \t", "{{ source | lstrip }}", 'source' => " \tab c \n \t" assert_template_result "ab c \n \t", '{{ source | lstrip }}', 'source' => " \tab c \n \t"
end end
def test_rstrip def test_rstrip
assert_template_result " ab c", "{{ source | rstrip }}", 'source' => " ab c " assert_template_result ' ab c', '{{ source | rstrip }}', 'source' => ' ab c '
assert_template_result " \tab c", "{{ source | rstrip }}", 'source' => " \tab c \n \t" assert_template_result " \tab c", '{{ source | rstrip }}', 'source' => " \tab c \n \t"
end end
def test_strip_newlines def test_strip_newlines
assert_template_result 'abc', "{{ source | strip_newlines }}", 'source' => "a\nb\nc" assert_template_result 'abc', '{{ source | strip_newlines }}', 'source' => "a\nb\nc"
assert_template_result 'abc', "{{ source | strip_newlines }}", 'source' => "a\r\nb\nc" assert_template_result 'abc', '{{ source | strip_newlines }}', 'source' => "a\r\nb\nc"
end end
def test_newlines_to_br def test_newlines_to_br
assert_template_result "a<br />\nb<br />\nc", "{{ source | newline_to_br }}", 'source' => "a\nb\nc" assert_template_result "a<br />\nb<br />\nc", '{{ source | newline_to_br }}', 'source' => "a\nb\nc"
end end
def test_plus def test_plus
assert_template_result "2", "{{ 1 | plus:1 }}" assert_template_result '2', '{{ 1 | plus:1 }}'
assert_template_result "2.0", "{{ '1' | plus:'1.0' }}" assert_template_result '2.0', "{{ '1' | plus:'1.0' }}"
assert_template_result "5", "{{ price | plus:'2' }}", 'price' => NumberLikeThing.new(3) assert_template_result '5', "{{ price | plus:'2' }}", 'price' => NumberLikeThing.new(3)
end end
def test_minus def test_minus
assert_template_result "4", "{{ input | minus:operand }}", 'input' => 5, 'operand' => 1 assert_template_result '4', '{{ input | minus:operand }}', 'input' => 5, 'operand' => 1
assert_template_result "2.3", "{{ '4.3' | minus:'2' }}" assert_template_result '2.3', "{{ '4.3' | minus:'2' }}"
assert_template_result "5", "{{ price | minus:'2' }}", 'price' => NumberLikeThing.new(7) assert_template_result '5', "{{ price | minus:'2' }}", 'price' => NumberLikeThing.new(7)
end end
def test_abs def test_abs
assert_template_result "17", "{{ 17 | abs }}" assert_template_result '17', '{{ 17 | abs }}'
assert_template_result "17", "{{ -17 | abs }}" assert_template_result '17', '{{ -17 | abs }}'
assert_template_result "17", "{{ '17' | abs }}" assert_template_result '17', "{{ '17' | abs }}"
assert_template_result "17", "{{ '-17' | abs }}" assert_template_result '17', "{{ '-17' | abs }}"
assert_template_result "0", "{{ 0 | abs }}" assert_template_result '0', '{{ 0 | abs }}'
assert_template_result "0", "{{ '0' | abs }}" assert_template_result '0', "{{ '0' | abs }}"
assert_template_result "17.42", "{{ 17.42 | abs }}" assert_template_result '17.42', '{{ 17.42 | abs }}'
assert_template_result "17.42", "{{ -17.42 | abs }}" assert_template_result '17.42', '{{ -17.42 | abs }}'
assert_template_result "17.42", "{{ '17.42' | abs }}" assert_template_result '17.42', "{{ '17.42' | abs }}"
assert_template_result "17.42", "{{ '-17.42' | abs }}" assert_template_result '17.42', "{{ '-17.42' | abs }}"
end end
def test_times def test_times
assert_template_result "12", "{{ 3 | times:4 }}" assert_template_result '12', '{{ 3 | times:4 }}'
assert_template_result "0", "{{ 'foo' | times:4 }}" assert_template_result '0', "{{ 'foo' | times:4 }}"
assert_template_result "6", "{{ '2.1' | times:3 | replace: '.','-' | plus:0}}" assert_template_result '6', "{{ '2.1' | times:3 | replace: '.','-' | plus:0}}"
assert_template_result "7.25", "{{ 0.0725 | times:100 }}" assert_template_result '7.25', '{{ 0.0725 | times:100 }}'
assert_template_result "-7.25", '{{ "-0.0725" | times:100 }}' assert_template_result '-7.25', '{{ "-0.0725" | times:100 }}'
assert_template_result "7.25", '{{ "-0.0725" | times: -100 }}' assert_template_result '7.25', '{{ "-0.0725" | times: -100 }}'
assert_template_result "4", "{{ price | times:2 }}", 'price' => NumberLikeThing.new(2) assert_template_result '4', '{{ price | times:2 }}', 'price' => NumberLikeThing.new(2)
end end
def test_divided_by def test_divided_by
assert_template_result "4", "{{ 12 | divided_by:3 }}" assert_template_result '4', '{{ 12 | divided_by:3 }}'
assert_template_result "4", "{{ 14 | divided_by:3 }}" assert_template_result '4', '{{ 14 | divided_by:3 }}'
assert_template_result "5", "{{ 15 | divided_by:3 }}" assert_template_result '5', '{{ 15 | divided_by:3 }}'
assert_equal "Liquid error: divided by 0", Template.parse("{{ 5 | divided_by:0 }}").render assert_equal 'Liquid error: divided by 0', Template.parse('{{ 5 | divided_by:0 }}').render
assert_template_result "0.5", "{{ 2.0 | divided_by:4 }}" assert_template_result '0.5', '{{ 2.0 | divided_by:4 }}'
assert_raises(Liquid::ZeroDivisionError) do assert_raises(Liquid::ZeroDivisionError) do
assert_template_result "4", "{{ 1 | modulo: 0 }}" assert_template_result '4', '{{ 1 | modulo: 0 }}'
end end
assert_template_result "5", "{{ price | divided_by:2 }}", 'price' => NumberLikeThing.new(10) assert_template_result '5', '{{ price | divided_by:2 }}', 'price' => NumberLikeThing.new(10)
end end
def test_modulo def test_modulo
assert_template_result "1", "{{ 3 | modulo:2 }}" assert_template_result '1', '{{ 3 | modulo:2 }}'
assert_raises(Liquid::ZeroDivisionError) do assert_raises(Liquid::ZeroDivisionError) do
assert_template_result "4", "{{ 1 | modulo: 0 }}" assert_template_result '4', '{{ 1 | modulo: 0 }}'
end end
assert_template_result "1", "{{ price | modulo:2 }}", 'price' => NumberLikeThing.new(3) assert_template_result '1', '{{ price | modulo:2 }}', 'price' => NumberLikeThing.new(3)
end end
def test_round def test_round
assert_template_result "5", "{{ input | round }}", 'input' => 4.6 assert_template_result '5', '{{ input | round }}', 'input' => 4.6
assert_template_result "4", "{{ '4.3' | round }}" assert_template_result '4', "{{ '4.3' | round }}"
assert_template_result "4.56", "{{ input | round: 2 }}", 'input' => 4.5612 assert_template_result '4.56', '{{ input | round: 2 }}', 'input' => 4.5612
assert_raises(Liquid::FloatDomainError) do assert_raises(Liquid::FloatDomainError) do
assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | round }}" assert_template_result '4', '{{ 1.0 | divided_by: 0.0 | round }}'
end end
assert_template_result "5", "{{ price | round }}", 'price' => NumberLikeThing.new(4.6) assert_template_result '5', '{{ price | round }}', 'price' => NumberLikeThing.new(4.6)
assert_template_result "4", "{{ price | round }}", 'price' => NumberLikeThing.new(4.3) assert_template_result '4', '{{ price | round }}', 'price' => NumberLikeThing.new(4.3)
end end
def test_ceil def test_ceil
assert_template_result "5", "{{ input | ceil }}", 'input' => 4.6 assert_template_result '5', '{{ input | ceil }}', 'input' => 4.6
assert_template_result "5", "{{ '4.3' | ceil }}" assert_template_result '5', "{{ '4.3' | ceil }}"
assert_raises(Liquid::FloatDomainError) do assert_raises(Liquid::FloatDomainError) do
assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | ceil }}" assert_template_result '4', '{{ 1.0 | divided_by: 0.0 | ceil }}'
end end
assert_template_result "5", "{{ price | ceil }}", 'price' => NumberLikeThing.new(4.6) assert_template_result '5', '{{ price | ceil }}', 'price' => NumberLikeThing.new(4.6)
end end
def test_floor def test_floor
assert_template_result "4", "{{ input | floor }}", 'input' => 4.6 assert_template_result '4', '{{ input | floor }}', 'input' => 4.6
assert_template_result "4", "{{ '4.3' | floor }}" assert_template_result '4', "{{ '4.3' | floor }}"
assert_raises(Liquid::FloatDomainError) do assert_raises(Liquid::FloatDomainError) do
assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | floor }}" assert_template_result '4', '{{ 1.0 | divided_by: 0.0 | floor }}'
end end
assert_template_result "5", "{{ price | floor }}", 'price' => NumberLikeThing.new(5.4) assert_template_result '5', '{{ price | floor }}', 'price' => NumberLikeThing.new(5.4)
end
def test_at_most
assert_template_result '4', '{{ 5 | at_most:4 }}'
assert_template_result '5', '{{ 5 | at_most:5 }}'
assert_template_result '5', '{{ 5 | at_most:6 }}'
assert_template_result '4.5', '{{ 4.5 | at_most:5 }}'
assert_template_result '5', '{{ width | at_most:5 }}', 'width' => NumberLikeThing.new(6)
assert_template_result '4', '{{ width | at_most:5 }}', 'width' => NumberLikeThing.new(4)
assert_template_result '4', '{{ 5 | at_most: width }}', 'width' => NumberLikeThing.new(4)
end
def test_at_least
assert_template_result '5', '{{ 5 | at_least:4 }}'
assert_template_result '5', '{{ 5 | at_least:5 }}'
assert_template_result '6', '{{ 5 | at_least:6 }}'
assert_template_result '5', '{{ 4.5 | at_least:5 }}'
assert_template_result '6', '{{ width | at_least:5 }}', 'width' => NumberLikeThing.new(6)
assert_template_result '5', '{{ width | at_least:5 }}', 'width' => NumberLikeThing.new(4)
assert_template_result '6', '{{ 5 | at_least: width }}', 'width' => NumberLikeThing.new(6)
end end
def test_append def test_append
assigns = { 'a' => 'bc', 'b' => 'd' } assigns = { 'a' => 'bc', 'b' => 'd' }
assert_template_result('bcd', "{{ a | append: 'd'}}", assigns) assert_template_result('bcd', "{{ a | append: 'd'}}", assigns)
assert_template_result('bcd', "{{ a | append: b}}", assigns) assert_template_result('bcd', '{{ a | append: b}}', assigns)
end end
def test_concat def test_concat
@@ -507,7 +663,7 @@ class StandardFiltersTest < Minitest::Test
assert_equal [1, 2, 'a'], @filters.concat([1, 2], ['a']) assert_equal [1, 2, 'a'], @filters.concat([1, 2], ['a'])
assert_equal [1, 2, 10], @filters.concat([1, 2], [10]) assert_equal [1, 2, 10], @filters.concat([1, 2], [10])
assert_raises(Liquid::ArgumentError, "concat filter requires an array argument") do assert_raises(Liquid::ArgumentError, 'concat filter requires an array argument') do
@filters.concat([1, 2], 10) @filters.concat([1, 2], 10)
end end
end end
@@ -515,16 +671,16 @@ class StandardFiltersTest < Minitest::Test
def test_prepend def test_prepend
assigns = { 'a' => 'bc', 'b' => 'a' } assigns = { 'a' => 'bc', 'b' => 'a' }
assert_template_result('abc', "{{ a | prepend: 'a'}}", assigns) assert_template_result('abc', "{{ a | prepend: 'a'}}", assigns)
assert_template_result('abc', "{{ a | prepend: b}}", assigns) assert_template_result('abc', '{{ a | prepend: b}}', assigns)
end end
def test_default def test_default
assert_equal "foo", @filters.default("foo", "bar") assert_equal 'foo', @filters.default('foo', 'bar')
assert_equal "bar", @filters.default(nil, "bar") assert_equal 'bar', @filters.default(nil, 'bar')
assert_equal "bar", @filters.default("", "bar") assert_equal 'bar', @filters.default('', 'bar')
assert_equal "bar", @filters.default(false, "bar") assert_equal 'bar', @filters.default(false, 'bar')
assert_equal "bar", @filters.default([], "bar") assert_equal 'bar', @filters.default([], 'bar')
assert_equal "bar", @filters.default({}, "bar") assert_equal 'bar', @filters.default({}, 'bar')
end end
def test_cannot_access_private_methods def test_cannot_access_private_methods
@@ -536,6 +692,78 @@ class StandardFiltersTest < Minitest::Test
assert_template_result('abc', "{{ 'abc' | date: '%D' }}") assert_template_result('abc', "{{ 'abc' | date: '%D' }}")
end end
def test_where
input = [
{ 'handle' => 'alpha', 'ok' => true },
{ 'handle' => 'beta', 'ok' => false },
{ 'handle' => 'gamma', 'ok' => false },
{ 'handle' => 'delta', 'ok' => true },
]
expectation = [
{ 'handle' => 'alpha', 'ok' => true },
{ 'handle' => 'delta', 'ok' => true },
]
assert_equal expectation, @filters.where(input, 'ok', true)
assert_equal expectation, @filters.where(input, 'ok')
end
def test_where_no_key_set
input = [
{ 'handle' => 'alpha', 'ok' => true },
{ 'handle' => 'beta' },
{ 'handle' => 'gamma' },
{ 'handle' => 'delta', 'ok' => true },
]
expectation = [
{ 'handle' => 'alpha', 'ok' => true },
{ 'handle' => 'delta', 'ok' => true },
]
assert_equal expectation, @filters.where(input, 'ok', true)
assert_equal expectation, @filters.where(input, 'ok')
end
def test_where_non_array_map_input
assert_equal [{ 'a' => 'ok' }], @filters.where({ 'a' => 'ok' }, 'a', 'ok')
assert_equal [], @filters.where({ 'a' => 'not ok' }, 'a', 'ok')
end
def test_where_indexable_but_non_map_value
assert_raises(Liquid::ArgumentError) { @filters.where(1, 'ok', true) }
assert_raises(Liquid::ArgumentError) { @filters.where(1, 'ok') }
end
def test_where_non_boolean_value
input = [
{ 'message' => 'Bonjour!', 'language' => 'French' },
{ 'message' => 'Hello!', 'language' => 'English' },
{ 'message' => 'Hallo!', 'language' => 'German' },
]
assert_equal [{ 'message' => 'Bonjour!', 'language' => 'French' }], @filters.where(input, 'language', 'French')
assert_equal [{ 'message' => 'Hallo!', 'language' => 'German' }], @filters.where(input, 'language', 'German')
assert_equal [{ 'message' => 'Hello!', 'language' => 'English' }], @filters.where(input, 'language', 'English')
end
def test_where_array_of_only_unindexable_values
assert_nil @filters.where([nil], 'ok', true)
assert_nil @filters.where([nil], 'ok')
end
def test_where_no_target_value
input = [
{ 'foo' => false },
{ 'foo' => true },
{ 'foo' => 'for sure' },
{ 'bar' => true },
]
assert_equal [{ 'foo' => true }, { 'foo' => 'for sure' }], @filters.where(input, 'foo')
end
private private
def with_timezone(tz) def with_timezone(tz)

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class BreakTagTest < Minitest::Test class BreakTagTest < Minitest::Test

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class ContinueTagTest < Minitest::Test class ContinueTagTest < Minitest::Test

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class ThingWithValue < Liquid::Drop class ThingWithValue < Liquid::Drop
@@ -23,16 +25,16 @@ class ForTagTest < Minitest::Test
yo yo
HERE HERE
template = <<HERE template = <<~HERE
{%for item in array%} {%for item in array%}
yo yo
{%endfor%} {%endfor%}
HERE HERE
assert_template_result(expected, template, 'array' => [1, 2, 3]) assert_template_result(expected, template, 'array' => [1, 2, 3])
end end
def test_for_reversed def test_for_reversed
assigns = { 'array' => [ 1, 2, 3] } assigns = { 'array' => [1, 2, 3] }
assert_template_result('321', '{%for item in array reversed %}{{item}}{%endfor%}', assigns) assert_template_result('321', '{%for item in array reversed %}{{item}}{%endfor%}', assigns)
end end
@@ -40,31 +42,31 @@ HERE
assert_template_result(' 1 2 3 ', '{%for item in (1..3) %} {{item}} {%endfor%}') assert_template_result(' 1 2 3 ', '{%for item in (1..3) %} {{item}} {%endfor%}')
assert_raises(Liquid::ArgumentError) do assert_raises(Liquid::ArgumentError) do
Template.parse('{% for i in (a..2) %}{% endfor %}').render!("a" => [1, 2]) Template.parse('{% for i in (a..2) %}{% endfor %}').render!('a' => [1, 2])
end end
assert_template_result(' 0 1 2 3 ', '{% for item in (a..3) %} {{item}} {% endfor %}', "a" => "invalid integer") assert_template_result(' 0 1 2 3 ', '{% for item in (a..3) %} {{item}} {% endfor %}', 'a' => 'invalid integer')
end end
def test_for_with_variable_range def test_for_with_variable_range
assert_template_result(' 1 2 3 ', '{%for item in (1..foobar) %} {{item}} {%endfor%}', "foobar" => 3) assert_template_result(' 1 2 3 ', '{%for item in (1..foobar) %} {{item}} {%endfor%}', 'foobar' => 3)
end end
def test_for_with_hash_value_range def test_for_with_hash_value_range
foobar = { "value" => 3 } foobar = { 'value' => 3 }
assert_template_result(' 1 2 3 ', '{%for item in (1..foobar.value) %} {{item}} {%endfor%}', "foobar" => foobar) assert_template_result(' 1 2 3 ', '{%for item in (1..foobar.value) %} {{item}} {%endfor%}', 'foobar' => foobar)
end end
def test_for_with_drop_value_range def test_for_with_drop_value_range
foobar = ThingWithValue.new foobar = ThingWithValue.new
assert_template_result(' 1 2 3 ', '{%for item in (1..foobar.value) %} {{item}} {%endfor%}', "foobar" => foobar) assert_template_result(' 1 2 3 ', '{%for item in (1..foobar.value) %} {{item}} {%endfor%}', 'foobar' => foobar)
end end
def test_for_with_variable def test_for_with_variable
assert_template_result(' 1 2 3 ', '{%for item in array%} {{item}} {%endfor%}', 'array' => [1, 2, 3]) assert_template_result(' 1 2 3 ', '{%for item in array%} {{item}} {%endfor%}', 'array' => [1, 2, 3])
assert_template_result('123', '{%for item in array%}{{item}}{%endfor%}', 'array' => [1, 2, 3]) assert_template_result('123', '{%for item in array%}{{item}}{%endfor%}', 'array' => [1, 2, 3])
assert_template_result('123', '{% for item in array %}{{item}}{% endfor %}', 'array' => [1, 2, 3]) assert_template_result('123', '{% for item in array %}{{item}}{% endfor %}', 'array' => [1, 2, 3])
assert_template_result('abcd', '{%for item in array%}{{item}}{%endfor%}', 'array' => ['a', 'b', 'c', 'd']) assert_template_result('abcd', '{%for item in array%}{{item}}{%endfor%}', 'array' => %w[a b c d])
assert_template_result('a b c', '{%for item in array%}{{item}}{%endfor%}', 'array' => ['a', ' ', 'b', ' ', 'c']) assert_template_result('a b c', '{%for item in array%}{{item}}{%endfor%}', 'array' => ['a', ' ', 'b', ' ', 'c'])
assert_template_result('abc', '{%for item in array%}{{item}}{%endfor%}', 'array' => ['a', '', 'b', '', 'c']) assert_template_result('abc', '{%for item in array%}{{item}}{%endfor%}', 'array' => ['a', '', 'b', '', 'c'])
end end
@@ -72,8 +74,8 @@ HERE
def test_for_helpers def test_for_helpers
assigns = { 'array' => [1, 2, 3] } assigns = { 'array' => [1, 2, 3] }
assert_template_result(' 1/3 2/3 3/3 ', assert_template_result(' 1/3 2/3 3/3 ',
'{%for item in array%} {{forloop.index}}/{{forloop.length}} {%endfor%}', '{%for item in array%} {{forloop.index}}/{{forloop.length}} {%endfor%}',
assigns) assigns)
assert_template_result(' 1 2 3 ', '{%for item in array%} {{forloop.index}} {%endfor%}', assigns) assert_template_result(' 1 2 3 ', '{%for item in array%} {{forloop.index}} {%endfor%}', assigns)
assert_template_result(' 0 1 2 ', '{%for item in array%} {{forloop.index0}} {%endfor%}', assigns) assert_template_result(' 0 1 2 ', '{%for item in array%} {{forloop.index0}} {%endfor%}', assigns)
assert_template_result(' 2 1 0 ', '{%for item in array%} {{forloop.rindex0}} {%endfor%}', assigns) assert_template_result(' 2 1 0 ', '{%for item in array%} {{forloop.rindex0}} {%endfor%}', assigns)
@@ -85,8 +87,8 @@ HERE
def test_for_and_if def test_for_and_if
assigns = { 'array' => [1, 2, 3] } assigns = { 'array' => [1, 2, 3] }
assert_template_result('+--', assert_template_result('+--',
'{%for item in array%}{% if forloop.first %}+{% else %}-{% endif %}{%endfor%}', '{%for item in array%}{% if forloop.first %}+{% else %}-{% endif %}{%endfor%}',
assigns) assigns)
end end
def test_for_else def test_for_else
@@ -103,6 +105,34 @@ HERE
assert_template_result('3456', '{%for i in array limit: 4 offset: 2 %}{{ i }}{%endfor%}', assigns) assert_template_result('3456', '{%for i in array limit: 4 offset: 2 %}{{ i }}{%endfor%}', assigns)
end end
def test_limiting_with_invalid_limit
assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] }
template = <<-MKUP
{% for i in array limit: true offset: 1 %}
{{ i }}
{% endfor %}
MKUP
exception = assert_raises(Liquid::ArgumentError) do
Template.parse(template).render!(assigns)
end
assert_equal('Liquid error: invalid integer', exception.message)
end
def test_limiting_with_invalid_offset
assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] }
template = <<-MKUP
{% for i in array limit: 1 offset: true %}
{{ i }}
{% endfor %}
MKUP
exception = assert_raises(Liquid::ArgumentError) do
Template.parse(template).render!(assigns)
end
assert_equal('Liquid error: invalid integer', exception.message)
end
def test_dynamic_variable_limiting def test_dynamic_variable_limiting
assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] }
assigns['limit'] = 2 assigns['limit'] = 2
@@ -129,14 +159,14 @@ HERE
{%for i in array.items offset:continue limit: 3 %}{{i}}{%endfor%} {%for i in array.items offset:continue limit: 3 %}{{i}}{%endfor%}
next next
{%for i in array.items offset:continue limit: 3 %}{{i}}{%endfor%} {%for i in array.items offset:continue limit: 3 %}{{i}}{%endfor%}
MKUP MKUP
expected = <<-XPCTD expected = <<-XPCTD
123 123
next next
456 456
next next
789 789
XPCTD XPCTD
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
end end
@@ -148,18 +178,18 @@ HERE
{%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%} {%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%}
next next
{%for i in array.items offset:continue limit:1 %}{{i}}{%endfor%} {%for i in array.items offset:continue limit:1 %}{{i}}{%endfor%}
MKUP MKUP
expected = <<-XPCTD expected = <<-XPCTD
123 123
next next
456 456
next next
7 7
XPCTD XPCTD
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
end end
def test_pause_resume_BIG_limit def test_pause_resume_big_limit
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } } assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } }
markup = <<-MKUP markup = <<-MKUP
{%for i in array.items limit:3 %}{{i}}{%endfor%} {%for i in array.items limit:3 %}{{i}}{%endfor%}
@@ -167,18 +197,18 @@ HERE
{%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%} {%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%}
next next
{%for i in array.items offset:continue limit:1000 %}{{i}}{%endfor%} {%for i in array.items offset:continue limit:1000 %}{{i}}{%endfor%}
MKUP MKUP
expected = <<-XPCTD expected = <<-XPCTD
123 123
next next
456 456
next next
7890 7890
XPCTD XPCTD
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
end end
def test_pause_resume_BIG_offset def test_pause_resume_big_offset
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } } assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } }
markup = '{%for i in array.items limit:3 %}{{i}}{%endfor%} markup = '{%for i in array.items limit:3 %}{{i}}{%endfor%}
next next
@@ -197,19 +227,19 @@ HERE
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] } } assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] } }
markup = '{% for i in array.items %}{% break %}{% endfor %}' markup = '{% for i in array.items %}{% break %}{% endfor %}'
expected = "" expected = ''
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
markup = '{% for i in array.items %}{{ i }}{% break %}{% endfor %}' markup = '{% for i in array.items %}{{ i }}{% break %}{% endfor %}'
expected = "1" expected = '1'
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
markup = '{% for i in array.items %}{% break %}{{ i }}{% endfor %}' markup = '{% for i in array.items %}{% break %}{{ i }}{% endfor %}'
expected = "" expected = ''
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
markup = '{% for i in array.items %}{{ i }}{% if i > 3 %}{% break %}{% endif %}{% endfor %}' markup = '{% for i in array.items %}{{ i }}{% if i > 3 %}{% break %}{% endif %}{% endfor %}'
expected = "1234" expected = '1234'
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
# tests to ensure it only breaks out of the local for loop # tests to ensure it only breaks out of the local for loop
@@ -237,23 +267,23 @@ HERE
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5] } } assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5] } }
markup = '{% for i in array.items %}{% continue %}{% endfor %}' markup = '{% for i in array.items %}{% continue %}{% endfor %}'
expected = "" expected = ''
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
markup = '{% for i in array.items %}{{ i }}{% continue %}{% endfor %}' markup = '{% for i in array.items %}{{ i }}{% continue %}{% endfor %}'
expected = "12345" expected = '12345'
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
markup = '{% for i in array.items %}{% continue %}{{ i }}{% endfor %}' markup = '{% for i in array.items %}{% continue %}{{ i }}{% endfor %}'
expected = "" expected = ''
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
markup = '{% for i in array.items %}{% if i > 3 %}{% continue %}{% endif %}{{ i }}{% endfor %}' markup = '{% for i in array.items %}{% if i > 3 %}{% continue %}{% endif %}{{ i }}{% endfor %}'
expected = "123" expected = '123'
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
markup = '{% for i in array.items %}{% if i == 3 %}{% continue %}{% else %}{{ i }}{% endif %}{% endfor %}' markup = '{% for i in array.items %}{% if i == 3 %}{% continue %}{% else %}{{ i }}{% endif %}{% endfor %}'
expected = "1245" expected = '1245'
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
# tests to ensure it only continues the local for loop and not all of them. # tests to ensure it only continues the local for loop and not all of them.
@@ -282,41 +312,41 @@ HERE
# the functionality for backwards compatibility # the functionality for backwards compatibility
assert_template_result('test string', assert_template_result('test string',
'{%for val in string%}{{val}}{%endfor%}', '{%for val in string%}{{val}}{%endfor%}',
'string' => "test string") 'string' => 'test string')
assert_template_result('test string', assert_template_result('test string',
'{%for val in string limit:1%}{{val}}{%endfor%}', '{%for val in string limit:1%}{{val}}{%endfor%}',
'string' => "test string") 'string' => 'test string')
assert_template_result('val-string-1-1-0-1-0-true-true-test string', assert_template_result('val-string-1-1-0-1-0-true-true-test string',
'{%for val in string%}' \ '{%for val in string%}' \
'{{forloop.name}}-' \ '{{forloop.name}}-' \
'{{forloop.index}}-' \ '{{forloop.index}}-' \
'{{forloop.length}}-' \ '{{forloop.length}}-' \
'{{forloop.index0}}-' \ '{{forloop.index0}}-' \
'{{forloop.rindex}}-' \ '{{forloop.rindex}}-' \
'{{forloop.rindex0}}-' \ '{{forloop.rindex0}}-' \
'{{forloop.first}}-' \ '{{forloop.first}}-' \
'{{forloop.last}}-' \ '{{forloop.last}}-' \
'{{val}}{%endfor%}', '{{val}}{%endfor%}',
'string' => "test string") 'string' => 'test string')
end end
def test_for_parentloop_references_parent_loop def test_for_parentloop_references_parent_loop
assert_template_result('1.1 1.2 1.3 2.1 2.2 2.3 ', assert_template_result('1.1 1.2 1.3 2.1 2.2 2.3 ',
'{% for inner in outer %}{% for k in inner %}' \ '{% for inner in outer %}{% for k in inner %}' \
'{{ forloop.parentloop.index }}.{{ forloop.index }} ' \ '{{ forloop.parentloop.index }}.{{ forloop.index }} ' \
'{% endfor %}{% endfor %}', '{% endfor %}{% endfor %}',
'outer' => [[1, 1, 1], [1, 1, 1]]) 'outer' => [[1, 1, 1], [1, 1, 1]])
end end
def test_for_parentloop_nil_when_not_present def test_for_parentloop_nil_when_not_present
assert_template_result('.1 .2 ', assert_template_result('.1 .2 ',
'{% for inner in outer %}' \ '{% for inner in outer %}' \
'{{ forloop.parentloop.index }}.{{ forloop.index }} ' \ '{{ forloop.parentloop.index }}.{{ forloop.index }} ' \
'{% endfor %}', '{% endfor %}',
'outer' => [[1, 1, 1], [1, 1, 1]]) 'outer' => [[1, 1, 1], [1, 1, 1]])
end end
def test_inner_for_over_empty_input def test_inner_for_over_empty_input
@@ -324,7 +354,7 @@ HERE
end end
def test_blank_string_not_iterable def test_blank_string_not_iterable
assert_template_result('', "{% for char in characters %}I WILL NOT BE OUTPUT{% endfor %}", 'characters' => '') assert_template_result('', '{% for char in characters %}I WILL NOT BE OUTPUT{% endfor %}', 'characters' => '')
end end
def test_bad_variable_naming_in_for_loop def test_bad_variable_naming_in_for_loop

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class IfElseTagTest < Minitest::Test class IfElseTagTest < Minitest::Test
@@ -6,7 +8,7 @@ class IfElseTagTest < Minitest::Test
def test_if def test_if
assert_template_result(' ', ' {% if false %} this text should not go into the output {% endif %} ') assert_template_result(' ', ' {% if false %} this text should not go into the output {% endif %} ')
assert_template_result(' this text should go into the output ', assert_template_result(' this text should go into the output ',
' {% if true %} this text should go into the output {% endif %} ') ' {% if true %} this text should go into the output {% endif %} ')
assert_template_result(' you rock ?', '{% if false %} you suck {% endif %} {% if true %} you rock {% endif %}?') assert_template_result(' you rock ?', '{% if false %} you suck {% endif %} {% if true %} you rock {% endif %}?')
end end
@@ -49,12 +51,12 @@ class IfElseTagTest < Minitest::Test
def test_comparison_of_expressions_starting_with_and_or_or def test_comparison_of_expressions_starting_with_and_or_or
assigns = { 'order' => { 'items_count' => 0 }, 'android' => { 'name' => 'Roy' } } assigns = { 'order' => { 'items_count' => 0 }, 'android' => { 'name' => 'Roy' } }
assert_template_result("YES", assert_template_result('YES',
"{% if android.name == 'Roy' %}YES{% endif %}", "{% if android.name == 'Roy' %}YES{% endif %}",
assigns) assigns)
assert_template_result("YES", assert_template_result('YES',
"{% if order.items_count == 0 %}YES{% endif %}", '{% if order.items_count == 0 %}YES{% endif %}',
assigns) assigns)
end end
def test_if_and def test_if_and
@@ -75,14 +77,14 @@ class IfElseTagTest < Minitest::Test
assert_template_result('', '{% if foo.bar %} NO {% endif %}', 'foo' => nil) assert_template_result('', '{% if foo.bar %} NO {% endif %}', 'foo' => nil)
assert_template_result('', '{% if foo.bar %} NO {% endif %}', 'foo' => true) assert_template_result('', '{% if foo.bar %} NO {% endif %}', 'foo' => true)
assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => "text") assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => 'text')
assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => true) assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => true)
assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => 1) assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => 1)
assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => {}) assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => {})
assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => []) assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => [])
assert_template_result(' YES ', '{% if "foo" %} YES {% endif %}') assert_template_result(' YES ', '{% if "foo" %} YES {% endif %}')
assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => true }) assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => true })
assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => "text" }) assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => 'text' })
assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => 1 }) assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => 1 })
assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => {} }) assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => {} })
assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => [] }) assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => [] })
@@ -90,11 +92,11 @@ class IfElseTagTest < Minitest::Test
assert_template_result(' YES ', '{% if var %} NO {% else %} YES {% endif %}', 'var' => false) assert_template_result(' YES ', '{% if var %} NO {% else %} YES {% endif %}', 'var' => false)
assert_template_result(' YES ', '{% if var %} NO {% else %} YES {% endif %}', 'var' => nil) assert_template_result(' YES ', '{% if var %} NO {% else %} YES {% endif %}', 'var' => nil)
assert_template_result(' YES ', '{% if var %} YES {% else %} NO {% endif %}', 'var' => true) assert_template_result(' YES ', '{% if var %} YES {% else %} NO {% endif %}', 'var' => true)
assert_template_result(' YES ', '{% if "foo" %} YES {% else %} NO {% endif %}', 'var' => "text") assert_template_result(' YES ', '{% if "foo" %} YES {% else %} NO {% endif %}', 'var' => 'text')
assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => { 'bar' => false }) assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => { 'bar' => false })
assert_template_result(' YES ', '{% if foo.bar %} YES {% else %} NO {% endif %}', 'foo' => { 'bar' => true }) assert_template_result(' YES ', '{% if foo.bar %} YES {% else %} NO {% endif %}', 'foo' => { 'bar' => true })
assert_template_result(' YES ', '{% if foo.bar %} YES {% else %} NO {% endif %}', 'foo' => { 'bar' => "text" }) assert_template_result(' YES ', '{% if foo.bar %} YES {% else %} NO {% endif %}', 'foo' => { 'bar' => 'text' })
assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => { 'notbar' => true }) assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => { 'notbar' => true })
assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => {}) assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => {})
assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', 'notfoo' => { 'bar' => true }) assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', 'notfoo' => { 'bar' => true })
@@ -132,7 +134,7 @@ class IfElseTagTest < Minitest::Test
end end
def test_syntax_error_no_variable def test_syntax_error_no_variable
assert_raises(SyntaxError){ assert_template_result('', '{% if jerry == 1 %}') } assert_raises(SyntaxError) { assert_template_result('', '{% if jerry == 1 %}') }
end end
def test_syntax_error_no_expression def test_syntax_error_no_expression
@@ -154,7 +156,7 @@ class IfElseTagTest < Minitest::Test
Condition.operators['contains'] = :[] Condition.operators['contains'] = :[]
assert_template_result('yes', assert_template_result('yes',
%({% if 'gnomeslab-and-or-liquid' contains 'gnomeslab-and-or-liquid' %}yes{% endif %})) %({% if 'gnomeslab-and-or-liquid' contains 'gnomeslab-and-or-liquid' %}yes{% endif %}))
ensure ensure
Condition.operators['contains'] = original_op Condition.operators['contains'] = original_op
end end
@@ -166,7 +168,7 @@ class IfElseTagTest < Minitest::Test
end end
def test_multiple_conditions def test_multiple_conditions
tpl = "{% if a or b and c %}true{% else %}false{% endif %}" tpl = '{% if a or b and c %}true{% else %}false{% endif %}'
tests = { tests = {
[true, true, true] => true, [true, true, true] => true,

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