mirror of
https://github.com/kemko/liquid.git
synced 2026-01-01 15:55:40 +03:00
Compare commits
275 Commits
lax-parse-
...
usage-trac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbea19b59f | ||
|
|
0521d78b30 | ||
|
|
34083c96d5 | ||
|
|
9672ed5285 | ||
|
|
f3112fc038 | ||
|
|
d338ccb9a6 | ||
|
|
b3097f143c | ||
|
|
7b309dc75d | ||
|
|
8f68cffdf1 | ||
|
|
dd27d0fd1d | ||
|
|
7a26e6b3d8 | ||
|
|
cf4e77ab0c | ||
|
|
7bae55dd39 | ||
|
|
0ce8aef229 | ||
|
|
6eab595fae | ||
|
|
b16b109a80 | ||
|
|
831355dfbd | ||
|
|
00702d8e63 | ||
|
|
197c058208 | ||
|
|
98dfe198e1 | ||
|
|
c2c1497ca8 | ||
|
|
d19967a79d | ||
|
|
248c54a386 | ||
|
|
2c42447659 | ||
|
|
ab698191b9 | ||
|
|
9ef6f9b642 | ||
|
|
4684478e94 | ||
|
|
b3b63a683f | ||
|
|
1c577c5b62 | ||
|
|
755d2821f3 | ||
|
|
495b3d312f | ||
|
|
9640e77805 | ||
|
|
453f6348c2 | ||
|
|
70ed1fc86d | ||
|
|
2a1ca3152d | ||
|
|
c2ef247be5 | ||
|
|
1518d3f6f9 | ||
|
|
c67b77709d | ||
|
|
c89ce9c2ed | ||
|
|
7dc488a73b | ||
|
|
e6ed804ca5 | ||
|
|
951abb67ee | ||
|
|
8d1cd41453 | ||
|
|
b0629f17f7 | ||
|
|
274f078806 | ||
|
|
d7171aa084 | ||
|
|
06c4789dc5 | ||
|
|
f2f467bdbc | ||
|
|
ff99d92c18 | ||
|
|
39fecd06db | ||
|
|
8013df8ca2 | ||
|
|
14cd011cb5 | ||
|
|
e2d9907df2 | ||
|
|
23d669f5e6 | ||
|
|
ed73794f82 | ||
|
|
f59f6dea83 | ||
|
|
7a81fb821a | ||
|
|
cec27ea326 | ||
|
|
14999e8f7c | ||
|
|
b41fc10d8e | ||
|
|
2b3c81cfd0 | ||
|
|
2a2376bfd9 | ||
|
|
ca9e75db53 | ||
|
|
407c8abf30 | ||
|
|
43f181e211 | ||
|
|
7c613e87cb | ||
|
|
fe4034ccf9 | ||
|
|
52ee303a36 | ||
|
|
8217a8d86c | ||
|
|
7d13d88258 | ||
|
|
ff727016ef | ||
|
|
c11fc656cf | ||
|
|
d789ec4175 | ||
|
|
fd09f049b0 | ||
|
|
842986a972 | ||
|
|
4661700a97 | ||
|
|
cd5a6dd225 | ||
|
|
89c1ba2b0e | ||
|
|
479d8fb4a4 | ||
|
|
53b8babf52 | ||
|
|
76b4920d3e | ||
|
|
8dcc319128 | ||
|
|
0b36461d80 | ||
|
|
70e75719de | ||
|
|
b037b19688 | ||
|
|
d0f77f6cf4 | ||
|
|
0be260bc97 | ||
|
|
5f0b64cebc | ||
|
|
c086017bc9 | ||
|
|
4369fe6c85 | ||
|
|
c118e6b435 | ||
|
|
0fbaf873d9 | ||
|
|
5980ddbfae | ||
|
|
193fc0fb7a | ||
|
|
e4da4d49d2 | ||
|
|
a0bec1f873 | ||
|
|
4aa3261518 | ||
|
|
04d552fabb | ||
|
|
5106466a2d | ||
|
|
5d6c1ed7c6 | ||
|
|
a594653a0c | ||
|
|
0c802aba17 | ||
|
|
147d7ae24d | ||
|
|
282d42f98d | ||
|
|
e6ba6ee87b | ||
|
|
2ad7a37d44 | ||
|
|
4bdaaf069f | ||
|
|
85b1e91aed | ||
|
|
a7c5e247c8 | ||
|
|
6c117fd7dd | ||
|
|
7d2d90d715 | ||
|
|
f761d21215 | ||
|
|
a796c17f8b | ||
|
|
deb10ebc7a | ||
|
|
cfe1844de9 | ||
|
|
59950bff87 | ||
|
|
27c91203ab | ||
|
|
44eaa4b9d8 | ||
|
|
a979b3ec95 | ||
|
|
bf3e759da3 | ||
|
|
59162f7a0e | ||
|
|
c582b86f16 | ||
|
|
e340803d12 | ||
|
|
48a6d86ac2 | ||
|
|
3bb29d5456 | ||
|
|
9c72ccb82f | ||
|
|
62d4625468 | ||
|
|
8928454e29 | ||
|
|
1370a102c9 | ||
|
|
c9bac9befe | ||
|
|
210a0616f3 | ||
|
|
5149cde5c3 | ||
|
|
22f2cec5de | ||
|
|
4318240ae0 | ||
|
|
aa79c33dda | ||
|
|
b1ef28566e | ||
|
|
41bcc48222 | ||
|
|
27d5106dc9 | ||
|
|
7334073be2 | ||
|
|
5dcefd7d77 | ||
|
|
25c7b05916 | ||
|
|
d17f86ba4d | ||
|
|
384e4313ff | ||
|
|
23f2af8ff5 | ||
|
|
a93eac0268 | ||
|
|
2cc7493cb0 | ||
|
|
85463e1753 | ||
|
|
52ff9b0e84 | ||
|
|
0c58328a40 | ||
|
|
2bb3552033 | ||
|
|
8b751ddf46 | ||
|
|
e5cbdb2b27 | ||
|
|
ffb0ace303 | ||
|
|
ad00998ef8 | ||
|
|
869dbc7ebf | ||
|
|
fae3a2de7b | ||
|
|
f27bd619b9 | ||
|
|
a9b84b7806 | ||
|
|
6cc2c567c5 | ||
|
|
812e3c51b9 | ||
|
|
9dd0801f5c | ||
|
|
b146b49f46 | ||
|
|
86944fe7b7 | ||
|
|
a549d289d7 | ||
|
|
b2feeacbce | ||
|
|
143ba39a08 | ||
|
|
43e59796f6 | ||
|
|
bb3624b799 | ||
|
|
64fca66ef5 | ||
|
|
e9d7486758 | ||
|
|
2bb98c1431 | ||
|
|
95d5c24bfc | ||
|
|
b7ee1a2176 | ||
|
|
0eca61a977 | ||
|
|
9bfd04da2d | ||
|
|
302185a7fc | ||
|
|
6ed6e7e12f | ||
|
|
f41ed78378 | ||
|
|
50c85afc35 | ||
|
|
5876dff326 | ||
|
|
f25185631d | ||
|
|
283f1bad18 | ||
|
|
e1d40c7d89 | ||
|
|
19c6eb426a | ||
|
|
f87b06095d | ||
|
|
b81d54e789 | ||
|
|
00f53b16e8 | ||
|
|
e4cf55b112 | ||
|
|
5bb211d933 | ||
|
|
6adc431a19 | ||
|
|
23d2beed41 | ||
|
|
a80ecb7678 | ||
|
|
361c695264 | ||
|
|
f93243cc1a | ||
|
|
1e533a52e7 | ||
|
|
3ea84f095f | ||
|
|
4239c899a4 | ||
|
|
1597f8859f | ||
|
|
b3dda384c9 | ||
|
|
6828670bfe | ||
|
|
d2f16d92d6 | ||
|
|
d233acb483 | ||
|
|
8920e2a2a2 | ||
|
|
bfee507005 | ||
|
|
929c89789f | ||
|
|
d03c4ae8e8 | ||
|
|
021bafd260 | ||
|
|
04c393ab07 | ||
|
|
9a7778e52c | ||
|
|
dde00253f9 | ||
|
|
18d1644980 | ||
|
|
c424d47274 | ||
|
|
8e6b9d503d | ||
|
|
8be38d1795 | ||
|
|
3146d5c3f2 | ||
|
|
0cc8b68a97 | ||
|
|
5a50c12953 | ||
|
|
a6fa4c5c38 | ||
|
|
dadd9b4dd2 | ||
|
|
6434b8d2bb | ||
|
|
2d891ddd8f | ||
|
|
60b508b151 | ||
|
|
3891f14a1a | ||
|
|
198f0aa366 | ||
|
|
f2e6adf566 | ||
|
|
08de6ed2c5 | ||
|
|
7e322f5cf8 | ||
|
|
bf86a5a069 | ||
|
|
0141444814 | ||
|
|
6d30226768 | ||
|
|
63e8bac1a4 | ||
|
|
8449849ed5 | ||
|
|
4bc198a0db | ||
|
|
3921dbe919 | ||
|
|
79e2d1d8b4 | ||
|
|
b7c4041db8 | ||
|
|
e113c891ec | ||
|
|
a32ad449c0 | ||
|
|
1662ba6679 | ||
|
|
99b5e86f0a | ||
|
|
b892a73463 | ||
|
|
0b55d09cea | ||
|
|
5f8086572b | ||
|
|
bdb9a4a47f | ||
|
|
c38eec0293 | ||
|
|
8d5a907dc8 | ||
|
|
74cc41ce74 | ||
|
|
a120cc587a | ||
|
|
c582023321 | ||
|
|
ac041c4ad1 | ||
|
|
31d7682f4e | ||
|
|
5f1acbc086 | ||
|
|
8612716129 | ||
|
|
e6392d1cc1 | ||
|
|
04381418d3 | ||
|
|
89ccdabe9a | ||
|
|
c0fc6777b0 | ||
|
|
cd03346239 | ||
|
|
b4f19da127 | ||
|
|
4100f8d641 | ||
|
|
d8bda2c892 | ||
|
|
4f81c0a658 | ||
|
|
704937bc00 | ||
|
|
27c6b8074a | ||
|
|
affae5ebef | ||
|
|
fc1c0d0d83 | ||
|
|
a215b70de9 | ||
|
|
1f70928f8a | ||
|
|
7713f6709d | ||
|
|
239cf0e5f5 | ||
|
|
fa187665b3 | ||
|
|
cd0c5e954c | ||
|
|
490b457738 | ||
|
|
4d6dec9b5a | ||
|
|
0b11b573d9 |
2
.github/probots.yml
vendored
Normal file
2
.github/probots.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
enabled:
|
||||||
|
- cla
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,3 +7,4 @@ pkg
|
|||||||
.ruby-version
|
.ruby-version
|
||||||
Gemfile.lock
|
Gemfile.lock
|
||||||
.bundle
|
.bundle
|
||||||
|
.byebug_history
|
||||||
|
|||||||
45
.rubocop.yml
45
.rubocop.yml
@@ -1,14 +1,20 @@
|
|||||||
inherit_from: ./.rubocop_todo.yml
|
inherit_from:
|
||||||
|
- .rubocop_todo.yml
|
||||||
|
- ./.rubocop_todo.yml
|
||||||
|
|
||||||
|
require: rubocop-performance
|
||||||
|
|
||||||
|
Performance:
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
AllCops:
|
AllCops:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'performance/shopify/*'
|
- 'performance/shopify/*'
|
||||||
|
- 'vendor/bundle/**/*'
|
||||||
- 'pkg/**'
|
- 'pkg/**'
|
||||||
|
|
||||||
Metrics/BlockNesting:
|
Metrics/BlockNesting:
|
||||||
Max: 3
|
Max: 3
|
||||||
Exclude:
|
|
||||||
- 'lib/liquid/block_body.rb'
|
|
||||||
|
|
||||||
Metrics/ModuleLength:
|
Metrics/ModuleLength:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
@@ -31,8 +37,8 @@ Lint/ParenthesesAsGroupedExpression:
|
|||||||
Lint/UnusedBlockArgument:
|
Lint/UnusedBlockArgument:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
Lint/EndAlignment:
|
Layout/EndAlignment:
|
||||||
AlignWith: variable
|
EnforcedStyleAlignWith: variable
|
||||||
|
|
||||||
Lint/UnusedMethodArgument:
|
Lint/UnusedMethodArgument:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
@@ -61,10 +67,10 @@ Style/BracesAroundHashParameters:
|
|||||||
Style/NumericLiterals:
|
Style/NumericLiterals:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
Style/SpaceInsideBrackets:
|
Layout/SpaceInsideArrayLiteralBrackets:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
Style/SpaceBeforeBlockBraces:
|
Layout/SpaceBeforeBlockBraces:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
Style/Documentation:
|
Style/Documentation:
|
||||||
@@ -73,19 +79,19 @@ Style/Documentation:
|
|||||||
Style/ClassAndModuleChildren:
|
Style/ClassAndModuleChildren:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
Style/TrailingComma:
|
Style/TrailingCommaInArrayLiteral:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
Style/IndentHash:
|
Style/TrailingCommaInHashLiteral:
|
||||||
EnforcedStyle: consistent
|
Enabled: false
|
||||||
|
|
||||||
Style/FormatString:
|
Style/FormatString:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
Style/AlignParameters:
|
Layout/AlignParameters:
|
||||||
EnforcedStyle: with_fixed_indentation
|
EnforcedStyle: with_fixed_indentation
|
||||||
|
|
||||||
Style/MultilineOperationIndentation:
|
Layout/MultilineOperationIndentation:
|
||||||
EnforcedStyle: indented
|
EnforcedStyle: indented
|
||||||
|
|
||||||
Style/IfUnlessModifier:
|
Style/IfUnlessModifier:
|
||||||
@@ -94,7 +100,7 @@ Style/IfUnlessModifier:
|
|||||||
Style/RaiseArgs:
|
Style/RaiseArgs:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
Style/DeprecatedHashMethods:
|
Style/PreferredHashMethods:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
Style/RegexpLiteral:
|
Style/RegexpLiteral:
|
||||||
@@ -103,13 +109,10 @@ Style/RegexpLiteral:
|
|||||||
Style/SymbolLiteral:
|
Style/SymbolLiteral:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
Performance/Count:
|
Naming/ConstantName:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
Style/ConstantName:
|
Layout/CaseIndentation:
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
Style/CaseIndentation:
|
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
Style/ClassVars:
|
Style/ClassVars:
|
||||||
@@ -118,6 +121,12 @@ Style/ClassVars:
|
|||||||
Style/PerlBackrefs:
|
Style/PerlBackrefs:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
Style/TrivialAccessors:
|
||||||
|
AllowPredicates: true
|
||||||
|
|
||||||
Style/WordArray:
|
Style/WordArray:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
Naming/MethodName:
|
||||||
|
Exclude:
|
||||||
|
- 'example/server/liquid_servlet.rb'
|
||||||
|
|||||||
@@ -1,72 +1,345 @@
|
|||||||
# This configuration was generated by `rubocop --auto-gen-config`
|
# This configuration was generated by
|
||||||
# on 2015-06-08 18:16:16 +0000 using RuboCop version 0.32.0.
|
# `rubocop --auto-gen-config`
|
||||||
|
# on 2019-08-29 00:43:36 +1000 using RuboCop version 0.74.0.
|
||||||
# The point is for the user to remove these configuration records
|
# The point is for the user to remove these configuration records
|
||||||
# one by one as the offenses are removed from the code base.
|
# one by one as the offenses are removed from the code base.
|
||||||
# Note that changes in the inspected code, or installation of new
|
# Note that changes in the inspected code, or installation of new
|
||||||
# versions of RuboCop, may require this file to be generated again.
|
# versions of RuboCop, may require this file to be generated again.
|
||||||
|
|
||||||
# Offense count: 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
|
# Offense count: 1
|
||||||
# Cop supports --auto-correct.
|
# Cop supports --auto-correct.
|
||||||
Style/ClosingParenthesisIndentation:
|
# Configuration parameters: TreatCommentsAsGroupSeparators, Include.
|
||||||
Enabled: false
|
# Include: **/*.gemspec
|
||||||
|
Gemspec/OrderedDependencies:
|
||||||
# Offense count: 3
|
Exclude:
|
||||||
# Configuration parameters: MinBodyLength.
|
- 'liquid.gemspec'
|
||||||
Style/GuardClause:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
# Offense count: 4
|
|
||||||
# Configuration parameters: EnforcedStyle, SupportedStyles.
|
|
||||||
Style/MethodName:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
# Offense count: 1
|
# Offense count: 1
|
||||||
Style/MultilineBlockChain:
|
# Configuration parameters: Include.
|
||||||
Enabled: false
|
# Include: **/*.gemspec,
|
||||||
|
Gemspec/RequiredRubyVersion:
|
||||||
|
Exclude:
|
||||||
|
- 'liquid.gemspec'
|
||||||
|
|
||||||
# Offense count: 2
|
# Offense count: 124
|
||||||
# Configuration parameters: EnforcedStyle, MinBodyLength, SupportedStyles.
|
# Cop supports --auto-correct.
|
||||||
Style/Next:
|
# Configuration parameters: EnforcedStyle, IndentationWidth.
|
||||||
|
# SupportedStyles: with_first_argument, with_fixed_indentation
|
||||||
|
Layout/AlignArguments:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
# Offense count: 7
|
# Offense count: 7
|
||||||
# Cop supports --auto-correct.
|
# Cop supports --auto-correct.
|
||||||
# Configuration parameters: AllowAsExpressionSeparator.
|
# Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle.
|
||||||
Style/Semicolon:
|
# SupportedHashRocketStyles: key, separator, table
|
||||||
|
# SupportedColonStyles: key, separator, table
|
||||||
|
# SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit
|
||||||
|
Layout/AlignHash:
|
||||||
|
Exclude:
|
||||||
|
- 'lib/liquid/condition.rb'
|
||||||
|
- 'lib/liquid/expression.rb'
|
||||||
|
- 'test/unit/context_unit_test.rb'
|
||||||
|
|
||||||
|
# Offense count: 6
|
||||||
|
# Cop supports --auto-correct.
|
||||||
|
Layout/ClosingHeredocIndentation:
|
||||||
|
Exclude:
|
||||||
|
- 'test/integration/tags/for_tag_test.rb'
|
||||||
|
|
||||||
|
# Offense count: 27
|
||||||
|
# Cop supports --auto-correct.
|
||||||
|
Layout/EmptyLineAfterGuardClause:
|
||||||
|
Exclude:
|
||||||
|
- 'lib/liquid/block.rb'
|
||||||
|
- 'lib/liquid/block_body.rb'
|
||||||
|
- 'lib/liquid/context.rb'
|
||||||
|
- 'lib/liquid/drop.rb'
|
||||||
|
- 'lib/liquid/lexer.rb'
|
||||||
|
- 'lib/liquid/parser.rb'
|
||||||
|
- 'lib/liquid/standardfilters.rb'
|
||||||
|
- 'lib/liquid/strainer.rb'
|
||||||
|
- 'lib/liquid/tags/for.rb'
|
||||||
|
- 'lib/liquid/tags/if.rb'
|
||||||
|
- 'lib/liquid/tags/include.rb'
|
||||||
|
- 'lib/liquid/utils.rb'
|
||||||
|
- 'lib/liquid/variable.rb'
|
||||||
|
- 'lib/liquid/variable_lookup.rb'
|
||||||
|
|
||||||
|
# Offense count: 5
|
||||||
|
# Cop supports --auto-correct.
|
||||||
|
# Configuration parameters: EnforcedStyle.
|
||||||
|
# SupportedStyles: squiggly, active_support, powerpack, unindent
|
||||||
|
Layout/IndentHeredoc:
|
||||||
|
Exclude:
|
||||||
|
- 'test/integration/tags/for_tag_test.rb'
|
||||||
|
- 'test/integration/trim_mode_test.rb'
|
||||||
|
|
||||||
|
# Offense count: 6
|
||||||
|
# Cop supports --auto-correct.
|
||||||
|
# Configuration parameters: EnforcedStyle.
|
||||||
|
# SupportedStyles: symmetrical, new_line, same_line
|
||||||
|
Layout/MultilineMethodCallBraceLayout:
|
||||||
|
Exclude:
|
||||||
|
- 'test/integration/error_handling_test.rb'
|
||||||
|
- 'test/unit/strainer_unit_test.rb'
|
||||||
|
|
||||||
|
# Offense count: 1
|
||||||
|
# Cop supports --auto-correct.
|
||||||
|
# Configuration parameters: AllowForAlignment.
|
||||||
|
Layout/SpaceAroundOperators:
|
||||||
|
Exclude:
|
||||||
|
- 'lib/liquid/condition.rb'
|
||||||
|
|
||||||
|
# Offense count: 2
|
||||||
|
# Cop supports --auto-correct.
|
||||||
|
# Configuration parameters: EnforcedStyle.
|
||||||
|
# SupportedStyles: runtime_error, standard_error
|
||||||
|
Lint/InheritException:
|
||||||
|
Exclude:
|
||||||
|
- 'lib/liquid/interrupts.rb'
|
||||||
|
|
||||||
|
# Offense count: 1
|
||||||
|
# Configuration parameters: CheckForMethodsWithNoSideEffects.
|
||||||
|
Lint/Void:
|
||||||
|
Exclude:
|
||||||
|
- 'lib/liquid/parse_context.rb'
|
||||||
|
|
||||||
|
# Offense count: 53
|
||||||
|
Metrics/AbcSize:
|
||||||
|
Max: 56
|
||||||
|
|
||||||
|
# Offense count: 12
|
||||||
|
Metrics/CyclomaticComplexity:
|
||||||
|
Max: 13
|
||||||
|
|
||||||
|
# Offense count: 118
|
||||||
|
# Configuration parameters: CountComments, ExcludedMethods.
|
||||||
|
Metrics/MethodLength:
|
||||||
|
Max: 38
|
||||||
|
|
||||||
|
# Offense count: 9
|
||||||
|
Metrics/PerceivedComplexity:
|
||||||
|
Max: 11
|
||||||
|
|
||||||
|
# Offense count: 1
|
||||||
|
# Cop supports --auto-correct.
|
||||||
|
# Configuration parameters: PreferredName.
|
||||||
|
Naming/RescuedExceptionsVariableName:
|
||||||
|
Exclude:
|
||||||
|
- 'lib/liquid/context.rb'
|
||||||
|
|
||||||
|
# Offense count: 20
|
||||||
|
# Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames.
|
||||||
|
# AllowedNames: io, id, to, by, on, in, at, ip, db
|
||||||
|
Naming/UncommunicativeMethodParamName:
|
||||||
|
Exclude:
|
||||||
|
- 'example/server/example_servlet.rb'
|
||||||
|
- 'lib/liquid/condition.rb'
|
||||||
|
- 'lib/liquid/context.rb'
|
||||||
|
- 'lib/liquid/standardfilters.rb'
|
||||||
|
- 'lib/liquid/tags/if.rb'
|
||||||
|
- 'lib/liquid/variable.rb'
|
||||||
|
- 'test/integration/filter_test.rb'
|
||||||
|
- 'test/integration/standard_filter_test.rb'
|
||||||
|
- 'test/integration/template_test.rb'
|
||||||
|
- 'test/unit/condition_unit_test.rb'
|
||||||
|
|
||||||
|
# Offense count: 5
|
||||||
|
# Configuration parameters: EnforcedStyle.
|
||||||
|
# SupportedStyles: inline, group
|
||||||
|
Style/AccessModifierDeclarations:
|
||||||
|
Exclude:
|
||||||
|
- 'lib/liquid/block_body.rb'
|
||||||
|
- 'lib/liquid/tag.rb'
|
||||||
|
- 'lib/liquid/tags/include.rb'
|
||||||
|
- 'test/unit/strainer_unit_test.rb'
|
||||||
|
|
||||||
|
# Offense count: 10
|
||||||
|
# Cop supports --auto-correct.
|
||||||
|
# Configuration parameters: EnforcedStyle.
|
||||||
|
# SupportedStyles: prefer_alias, prefer_alias_method
|
||||||
|
Style/Alias:
|
||||||
|
Exclude:
|
||||||
|
- 'lib/liquid/drop.rb'
|
||||||
|
- 'lib/liquid/i18n.rb'
|
||||||
|
- 'lib/liquid/profiler/hooks.rb'
|
||||||
|
- 'lib/liquid/standardfilters.rb'
|
||||||
|
- 'lib/liquid/tag.rb'
|
||||||
|
- 'lib/liquid/tags/include.rb'
|
||||||
|
- 'lib/liquid/variable.rb'
|
||||||
|
|
||||||
|
# Offense count: 22
|
||||||
|
Style/CommentedKeyword:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
# Offense count: 1
|
# Offense count: 1
|
||||||
# Cop supports --auto-correct.
|
# Cop supports --auto-correct.
|
||||||
# Configuration parameters: MaxLineLength.
|
# Configuration parameters: EnforcedStyle, SingleLineConditionsOnly, IncludeTernaryExpressions.
|
||||||
Style/WhileUntilModifier:
|
# SupportedStyles: assign_to_condition, assign_inside_condition
|
||||||
|
Style/ConditionalAssignment:
|
||||||
|
Exclude:
|
||||||
|
- 'lib/liquid/errors.rb'
|
||||||
|
|
||||||
|
# Offense count: 1
|
||||||
|
# Cop supports --auto-correct.
|
||||||
|
Style/EmptyCaseCondition:
|
||||||
|
Exclude:
|
||||||
|
- 'lib/liquid/lexer.rb'
|
||||||
|
|
||||||
|
# Offense count: 5
|
||||||
|
# Cop supports --auto-correct.
|
||||||
|
# Configuration parameters: EnforcedStyle.
|
||||||
|
# SupportedStyles: compact, expanded
|
||||||
|
Style/EmptyMethod:
|
||||||
|
Exclude:
|
||||||
|
- 'lib/liquid/tag.rb'
|
||||||
|
- 'lib/liquid/tags/comment.rb'
|
||||||
|
- 'lib/liquid/tags/include.rb'
|
||||||
|
- 'test/integration/tags/include_tag_test.rb'
|
||||||
|
- 'test/unit/context_unit_test.rb'
|
||||||
|
|
||||||
|
# Offense count: 3
|
||||||
|
# Cop supports --auto-correct.
|
||||||
|
Style/Encoding:
|
||||||
|
Exclude:
|
||||||
|
- 'lib/liquid/version.rb'
|
||||||
|
- 'liquid.gemspec'
|
||||||
|
- 'test/integration/standard_filter_test.rb'
|
||||||
|
|
||||||
|
# Offense count: 2
|
||||||
|
# Cop supports --auto-correct.
|
||||||
|
Style/ExpandPathArguments:
|
||||||
|
Exclude:
|
||||||
|
- 'Rakefile'
|
||||||
|
- 'liquid.gemspec'
|
||||||
|
|
||||||
|
# Offense count: 7
|
||||||
|
# Configuration parameters: EnforcedStyle.
|
||||||
|
# SupportedStyles: annotated, template, unannotated
|
||||||
|
Style/FormatStringToken:
|
||||||
|
Exclude:
|
||||||
|
- 'test/integration/filter_test.rb'
|
||||||
|
- 'test/integration/hash_ordering_test.rb'
|
||||||
|
|
||||||
|
# Offense count: 106
|
||||||
|
# Cop supports --auto-correct.
|
||||||
|
# Configuration parameters: EnforcedStyle.
|
||||||
|
# SupportedStyles: always, never
|
||||||
|
Style/FrozenStringLiteralComment:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
# Offense count: 14
|
||||||
|
# Configuration parameters: MinBodyLength.
|
||||||
|
Style/GuardClause:
|
||||||
|
Exclude:
|
||||||
|
- 'lib/liquid/condition.rb'
|
||||||
|
- 'lib/liquid/lexer.rb'
|
||||||
|
- 'lib/liquid/strainer.rb'
|
||||||
|
- 'lib/liquid/tags/assign.rb'
|
||||||
|
- 'lib/liquid/tags/capture.rb'
|
||||||
|
- 'lib/liquid/tags/case.rb'
|
||||||
|
- 'lib/liquid/tags/for.rb'
|
||||||
|
- 'lib/liquid/tags/include.rb'
|
||||||
|
- 'lib/liquid/tags/raw.rb'
|
||||||
|
- 'lib/liquid/tags/table_row.rb'
|
||||||
|
- 'lib/liquid/variable.rb'
|
||||||
|
- 'test/unit/tokenizer_unit_test.rb'
|
||||||
|
|
||||||
|
# Offense count: 53
|
||||||
|
# Cop supports --auto-correct.
|
||||||
|
# Configuration parameters: EnforcedStyle.
|
||||||
|
# SupportedStyles: literals, strict
|
||||||
|
Style/MutableConstant:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
# Offense count: 1
|
||||||
|
# Cop supports --auto-correct.
|
||||||
|
# Configuration parameters: EnforcedStyle, MinBodyLength.
|
||||||
|
# SupportedStyles: skip_modifier_ifs, always
|
||||||
|
Style/Next:
|
||||||
|
Exclude:
|
||||||
|
- 'lib/liquid/tags/for.rb'
|
||||||
|
|
||||||
|
# Offense count: 13
|
||||||
|
# Cop supports --auto-correct.
|
||||||
|
# Configuration parameters: AutoCorrect, EnforcedStyle, IgnoredMethods.
|
||||||
|
# SupportedStyles: predicate, comparison
|
||||||
|
Style/NumericPredicate:
|
||||||
|
Exclude:
|
||||||
|
- 'spec/**/*'
|
||||||
|
- 'lib/liquid/context.rb'
|
||||||
|
- 'lib/liquid/forloop_drop.rb'
|
||||||
|
- 'lib/liquid/standardfilters.rb'
|
||||||
|
- 'lib/liquid/tablerowloop_drop.rb'
|
||||||
|
- 'test/integration/standard_filter_test.rb'
|
||||||
|
- 'test/integration/template_test.rb'
|
||||||
|
|
||||||
|
# Offense count: 14
|
||||||
|
# Cop supports --auto-correct.
|
||||||
|
# Configuration parameters: PreferredDelimiters.
|
||||||
|
Style/PercentLiteralDelimiters:
|
||||||
|
Exclude:
|
||||||
|
- 'lib/liquid/tags/if.rb'
|
||||||
|
- 'liquid.gemspec'
|
||||||
|
- 'test/integration/assign_test.rb'
|
||||||
|
- 'test/integration/standard_filter_test.rb'
|
||||||
|
|
||||||
|
# Offense count: 1
|
||||||
|
# Cop supports --auto-correct.
|
||||||
|
Style/RedundantSelf:
|
||||||
|
Exclude:
|
||||||
|
- 'lib/liquid/strainer.rb'
|
||||||
|
|
||||||
|
# Offense count: 3
|
||||||
|
# Cop supports --auto-correct.
|
||||||
|
# Configuration parameters: ConvertCodeThatCanStartToReturnNil, Whitelist.
|
||||||
|
# Whitelist: present?, blank?, presence, try, try!
|
||||||
|
Style/SafeNavigation:
|
||||||
|
Exclude:
|
||||||
|
- 'lib/liquid/drop.rb'
|
||||||
|
- 'lib/liquid/strainer.rb'
|
||||||
|
- 'lib/liquid/tokenizer.rb'
|
||||||
|
|
||||||
|
# Offense count: 9
|
||||||
|
# Cop supports --auto-correct.
|
||||||
|
# Configuration parameters: AllowAsExpressionSeparator.
|
||||||
|
Style/Semicolon:
|
||||||
|
Exclude:
|
||||||
|
- 'test/integration/error_handling_test.rb'
|
||||||
|
- 'test/integration/template_test.rb'
|
||||||
|
- 'test/unit/context_unit_test.rb'
|
||||||
|
|
||||||
|
# Offense count: 7
|
||||||
|
# Cop supports --auto-correct.
|
||||||
|
# Configuration parameters: MinSize.
|
||||||
|
# SupportedStyles: percent, brackets
|
||||||
|
Style/SymbolArray:
|
||||||
|
EnforcedStyle: brackets
|
||||||
|
|
||||||
|
# Offense count: 2
|
||||||
|
# Cop supports --auto-correct.
|
||||||
|
# Configuration parameters: EnforcedStyle, AllowSafeAssignment.
|
||||||
|
# SupportedStyles: require_parentheses, require_no_parentheses, require_parentheses_when_complex
|
||||||
|
Style/TernaryParentheses:
|
||||||
|
Exclude:
|
||||||
|
- 'lib/liquid/context.rb'
|
||||||
|
- 'lib/liquid/utils.rb'
|
||||||
|
|
||||||
|
# Offense count: 2
|
||||||
|
# Cop supports --auto-correct.
|
||||||
|
Style/UnneededPercentQ:
|
||||||
|
Exclude:
|
||||||
|
- 'test/integration/error_handling_test.rb'
|
||||||
|
|
||||||
|
# Offense count: 1
|
||||||
|
# Cop supports --auto-correct.
|
||||||
|
Style/WhileUntilModifier:
|
||||||
|
Exclude:
|
||||||
|
- 'lib/liquid/tags/case.rb'
|
||||||
|
|
||||||
|
# Offense count: 665
|
||||||
|
# Cop supports --auto-correct.
|
||||||
|
# Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
|
||||||
|
# URISchemes: http, https
|
||||||
|
Metrics/LineLength:
|
||||||
|
Max: 294
|
||||||
|
|||||||
24
.travis.yml
24
.travis.yml
@@ -1,20 +1,30 @@
|
|||||||
language: ruby
|
language: ruby
|
||||||
|
cache: bundler
|
||||||
|
|
||||||
rvm:
|
rvm:
|
||||||
- 2.0
|
- 2.4
|
||||||
- 2.1
|
- 2.5
|
||||||
- 2.2
|
- &latest_ruby 2.6
|
||||||
|
- 2.7
|
||||||
- ruby-head
|
- ruby-head
|
||||||
- jruby-head
|
- jruby-head
|
||||||
# - rbx-2
|
- truffleruby
|
||||||
|
|
||||||
sudo: false
|
|
||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
|
include:
|
||||||
|
- rvm: *latest_ruby
|
||||||
|
script: bundle exec rake memory_profile:run
|
||||||
|
name: Profiling Memory Usage
|
||||||
allow_failures:
|
allow_failures:
|
||||||
|
- rvm: ruby-head
|
||||||
- rvm: jruby-head
|
- rvm: jruby-head
|
||||||
|
- rvm: truffleruby
|
||||||
|
|
||||||
script: "bundle exec rake"
|
branches:
|
||||||
|
only:
|
||||||
|
- master
|
||||||
|
- gh-pages
|
||||||
|
- /.*-stable/
|
||||||
|
|
||||||
notifications:
|
notifications:
|
||||||
disable: true
|
disable: true
|
||||||
|
|||||||
@@ -4,23 +4,22 @@
|
|||||||
|
|
||||||
* Bugfixes
|
* Bugfixes
|
||||||
* Performance improvements
|
* Performance improvements
|
||||||
* Features which are likely to be useful to the majority of Liquid users
|
* Features that are likely to be useful to the majority of Liquid users
|
||||||
|
|
||||||
## Things we won't merge
|
## Things we won't merge
|
||||||
|
|
||||||
* Code which introduces considerable performance degrations
|
* Code that introduces considerable performance degrations
|
||||||
* Code which touches performance critical parts of Liquid and comes without benchmarks
|
* Code that touches performance-critical parts of Liquid and comes without benchmarks
|
||||||
* Features which are not important for most people (we want to keep the core Liquid code small and tidy)
|
* Features that are not important for most people (we want to keep the core Liquid code small and tidy)
|
||||||
* Features which can easily be implemented on top of Liquid (for example as a custom filter or custom filesystem)
|
* Features that can easily be implemented on top of Liquid (for example as a custom filter or custom filesystem)
|
||||||
* Code which comes without tests
|
* Code that does not include tests
|
||||||
* Code which breaks existing tests
|
* Code that breaks existing tests
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
* Fork the Liquid repository
|
* Fork the Liquid repository
|
||||||
* Create a new branch in your fork
|
* Create a new branch in your fork
|
||||||
* If it makes sense, add tests for your code and run a performance benchmark
|
* If it makes sense, add tests for your code and/or run a performance benchmark
|
||||||
* Make sure all tests pass
|
* Make sure all tests pass (`bundle exec rake`)
|
||||||
* Create a pull request
|
* Create a pull request
|
||||||
* In the description, ping one of [@boourns](https://github.com/boourns), [@fw42](https://github.com/fw42), [@camilo](https://github.com/camilo), [@dylanahsmith](https://github.com/dylanahsmith), or [@arthurnn](https://github.com/arthurnn) and ask for a code review.
|
|
||||||
|
|
||||||
|
|||||||
23
Gemfile
23
Gemfile
@@ -1,14 +1,25 @@
|
|||||||
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_21
|
|
||||||
|
|
||||||
group :test do
|
group :benchmark, :test do
|
||||||
gem 'spy', '0.4.1'
|
|
||||||
gem 'benchmark-ips'
|
gem 'benchmark-ips'
|
||||||
gem 'rubocop', '>=0.32.0'
|
gem 'memory_profiler'
|
||||||
|
gem 'terminal-table'
|
||||||
|
|
||||||
platform :mri do
|
install_if -> { RUBY_PLATFORM !~ /mingw|mswin|java/ && RUBY_ENGINE != 'truffleruby' } do
|
||||||
gem 'liquid-c', github: 'Shopify/liquid-c', ref: '2570693d8d03faa0df9160ec74348a7149436df3'
|
gem 'stackprof'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
group :test do
|
||||||
|
gem 'rubocop', '~> 0.74.0', require: false
|
||||||
|
gem 'rubocop-performance', require: false
|
||||||
|
|
||||||
|
platform :mri, :truffleruby do
|
||||||
|
gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'liquid-tag'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
94
History.md
94
History.md
@@ -1,8 +1,69 @@
|
|||||||
# Liquid Change Log
|
# Liquid Change Log
|
||||||
|
|
||||||
## 4.0.0 / not yet released / branch "master"
|
## 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
|
### 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"
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* Render an opaque internal error by default for non-Liquid::Error (#835) [Dylan Thacker-Smith]
|
||||||
|
* Ruby 2.0 support dropped (#832) [Dylan Thacker-Smith]
|
||||||
|
* Add to_number Drop method to allow custom drops to work with number filters (#731)
|
||||||
|
* Add strict_variables and strict_filters options to detect undefined references (#691)
|
||||||
|
* Improve loop performance (#681) [Florian Weingarten]
|
||||||
|
* Rename Drop method `before_method` to `liquid_method_missing` (#661) [Thierry Joyal]
|
||||||
|
* Add url_decode filter to invert url_encode (#645) [Larry Archer]
|
||||||
|
* Add global_filter to apply a filter to all output (#610) [Loren Hale]
|
||||||
* Add compact filter (#600) [Carson Reinke]
|
* Add compact filter (#600) [Carson Reinke]
|
||||||
* Rename deprecated "has_key?" and "has_interrupt?" methods (#593) [Florian Weingarten]
|
* Rename deprecated "has_key?" and "has_interrupt?" methods (#593) [Florian Weingarten]
|
||||||
* Include template name with line numbers in render errors (574) [Dylan Thacker-Smith]
|
* Include template name with line numbers in render errors (574) [Dylan Thacker-Smith]
|
||||||
@@ -12,9 +73,18 @@
|
|||||||
* Add concat filter to concatenate arrays (#429) [Diogo Beato]
|
* Add concat filter to concatenate arrays (#429) [Diogo Beato]
|
||||||
* Ruby 1.9 support dropped (#491) [Justin Li]
|
* Ruby 1.9 support dropped (#491) [Justin Li]
|
||||||
* Liquid::Template.file_system's read_template_file method is no longer passed the context. (#441) [James Reid-Smith]
|
* Liquid::Template.file_system's read_template_file method is no longer passed the context. (#441) [James Reid-Smith]
|
||||||
* Remove support for `liquid_methods`
|
* Remove `liquid_methods` (See https://github.com/Shopify/liquid/pull/568 for replacement)
|
||||||
|
* Liquid::Template.register_filter raises when the module overrides registered public methods as private or protected (#705) [Gaurav Chande]
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
* Fix variable names being detected as an operator when starting with contains (#788) [Michael Angell]
|
||||||
|
* Fix include tag used with strict_variables (#828) [QuickPay]
|
||||||
|
* Fix map filter when value is a Proc (#672) [Guillaume Malette]
|
||||||
|
* Fix truncate filter when value is not a string (#672) [Guillaume Malette]
|
||||||
|
* Fix behaviour of escape filter when input is nil (#665) [Tanel Jakobsoo]
|
||||||
|
* Fix sort filter behaviour with empty array input (#652) [Marcel Cary]
|
||||||
|
* Fix test failure under certain timezones (#631) [Dylan Thacker-Smith]
|
||||||
* Fix bug in uniq filter (#595) [Florian Weingarten]
|
* Fix bug in uniq filter (#595) [Florian Weingarten]
|
||||||
* Fix bug when "blank" and "empty" are used as variable names (#592) [Florian Weingarten]
|
* Fix bug when "blank" and "empty" are used as variable names (#592) [Florian Weingarten]
|
||||||
* Fix condition parse order in strict mode (#569) [Justin Li]
|
* Fix condition parse order in strict mode (#569) [Justin Li]
|
||||||
@@ -26,7 +96,15 @@
|
|||||||
* Disallow variable names in the strict parser that are not valid in the lax parser (#463) [Justin Li]
|
* Disallow variable names in the strict parser that are not valid in the lax parser (#463) [Justin Li]
|
||||||
* Fix BlockBody#warnings taking exponential time to compute (#486) [Justin Li]
|
* Fix BlockBody#warnings taking exponential time to compute (#486) [Justin Li]
|
||||||
|
|
||||||
## 3.0.3 / 2015-05-28 / branch "3-0-stable"
|
## 3.0.5 / 2015-07-23 / branch "3-0-stable"
|
||||||
|
|
||||||
|
* Fix test failure under certain timezones [Dylan Thacker-Smith]
|
||||||
|
|
||||||
|
## 3.0.4 / 2015-07-17
|
||||||
|
|
||||||
|
* Fix chained access to multi-dimensional hashes [Florian Weingarten]
|
||||||
|
|
||||||
|
## 3.0.3 / 2015-05-28
|
||||||
|
|
||||||
* Fix condition parse order in strict mode (#569) [Justin Li]
|
* Fix condition parse order in strict mode (#569) [Justin Li]
|
||||||
|
|
||||||
@@ -74,7 +152,15 @@
|
|||||||
* Make map filter work on enumerable drops (#233) [Florian Weingarten]
|
* Make map filter work on enumerable drops (#233) [Florian Weingarten]
|
||||||
* Improved whitespace stripping for blank blocks, related to #216 [Florian Weingarten]
|
* Improved whitespace stripping for blank blocks, related to #216 [Florian Weingarten]
|
||||||
|
|
||||||
## 2.6.1 / 2014-01-10 / branch "2-6-stable"
|
## 2.6.3 / 2015-07-23 / branch "2-6-stable"
|
||||||
|
|
||||||
|
* Fix test failure under certain timezones [Dylan Thacker-Smith]
|
||||||
|
|
||||||
|
## 2.6.2 / 2015-01-23
|
||||||
|
|
||||||
|
* Remove duplicate hash key [Parker Moore]
|
||||||
|
|
||||||
|
## 2.6.1 / 2014-01-10
|
||||||
|
|
||||||
Security fix, cherry-picked from master (4e14a65):
|
Security fix, cherry-picked from master (4e14a65):
|
||||||
* Don't call to_sym when creating conditions for security reasons (#273) [Bouke van der Bijl]
|
* Don't call to_sym when creating conditions for security reasons (#273) [Bouke van der Bijl]
|
||||||
|
|||||||
33
README.md
33
README.md
@@ -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.
|
||||||
|
|
||||||
@@ -73,3 +75,34 @@ This is useful for doing things like enabling strict mode only in the theme edit
|
|||||||
|
|
||||||
It is recommended that you enable `:strict` or `:warn` mode on new apps to stop invalid templates from being created.
|
It is recommended that you enable `:strict` or `:warn` mode on new apps to stop invalid templates from being created.
|
||||||
It is also recommended that you use it in the template editors of existing apps to give editors better error messages.
|
It is also recommended that you use it in the template editors of existing apps to give editors better error messages.
|
||||||
|
|
||||||
|
### Undefined variables and filters
|
||||||
|
|
||||||
|
By default, the renderer doesn't raise or in any other way notify you if some variables or filters are missing, i.e. not passed to the `render` method.
|
||||||
|
You can improve this situation by passing `strict_variables: true` and/or `strict_filters: true` options to the `render` method.
|
||||||
|
When one of these options is set to true, all errors about undefined variables and undefined filters will be stored in `errors` array of a `Liquid::Template` instance.
|
||||||
|
Here are some examples:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
template = Liquid::Template.parse("{{x}} {{y}} {{z.a}} {{z.b}}")
|
||||||
|
template.render({ 'x' => 1, 'z' => { 'a' => 2 } }, { strict_variables: true })
|
||||||
|
#=> '1 2 ' # when a variable is undefined, it's rendered as nil
|
||||||
|
template.errors
|
||||||
|
#=> [#<Liquid::UndefinedVariable: Liquid error: undefined variable y>, #<Liquid::UndefinedVariable: Liquid error: undefined variable b>]
|
||||||
|
```
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
template = Liquid::Template.parse("{{x | filter1 | upcase}}")
|
||||||
|
template.render({ 'x' => 'foo' }, { strict_filters: true })
|
||||||
|
#=> '' # when at least one filter in the filter chain is undefined, a whole expression is rendered as nil
|
||||||
|
template.errors
|
||||||
|
#=> [#<Liquid::UndefinedFilter: Liquid error: undefined filter filter1>]
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to raise on a first exception instead of pushing all of them in `errors`, you can use `render!` method:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
template = Liquid::Template.parse("{{x}} {{y}}")
|
||||||
|
template.render!({ 'x' => 1}, { strict_variables: true })
|
||||||
|
#=> Liquid::UndefinedVariable: Liquid error: undefined variable y
|
||||||
|
```
|
||||||
|
|||||||
19
Rakefile
19
Rakefile
@@ -3,7 +3,7 @@ require 'rake/testtask'
|
|||||||
$LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
|
$LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
|
||||||
require "liquid/version"
|
require "liquid/version"
|
||||||
|
|
||||||
task default: [:rubocop, :test]
|
task default: [:test, :rubocop]
|
||||||
|
|
||||||
desc 'run test suite with default parser'
|
desc 'run test suite with default parser'
|
||||||
Rake::TestTask.new(:base_test) do |t|
|
Rake::TestTask.new(:base_test) do |t|
|
||||||
@@ -19,8 +19,10 @@ task :warn_test do
|
|||||||
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'
|
||||||
@@ -32,8 +34,8 @@ 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
|
||||||
@@ -85,6 +87,13 @@ namespace :profile do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
namespace :memory_profile do
|
||||||
|
desc "Run memory profiler"
|
||||||
|
task :run do
|
||||||
|
ruby "./performance/memory_profile.rb"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
desc "Run example"
|
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"
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
machine:
|
|
||||||
ruby:
|
|
||||||
version: ruby-2.0
|
|
||||||
@@ -24,6 +24,7 @@ module Liquid
|
|||||||
ArgumentSeparator = ','.freeze
|
ArgumentSeparator = ','.freeze
|
||||||
FilterArgumentSeparator = ':'.freeze
|
FilterArgumentSeparator = ':'.freeze
|
||||||
VariableAttributeSeparator = '.'.freeze
|
VariableAttributeSeparator = '.'.freeze
|
||||||
|
WhitespaceControl = '-'.freeze
|
||||||
TagStart = /\{\%/
|
TagStart = /\{\%/
|
||||||
TagEnd = /\%\}/
|
TagEnd = /\%\}/
|
||||||
VariableSignature = /\(?[\w\-\.\[\]]\)?/
|
VariableSignature = /\(?[\w\-\.\[\]]\)?/
|
||||||
@@ -34,7 +35,7 @@ module Liquid
|
|||||||
QuotedString = /"[^"]*"|'[^']*'/
|
QuotedString = /"[^"]*"|'[^']*'/
|
||||||
QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o
|
QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o
|
||||||
TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/o
|
TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/o
|
||||||
AnyStartingTag = /\{\{|\{\%/
|
AnyStartingTag = /#{TagStart}|#{VariableStart}/o
|
||||||
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om
|
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om
|
||||||
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om
|
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om
|
||||||
VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o
|
VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o
|
||||||
@@ -44,10 +45,13 @@ module Liquid
|
|||||||
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'
|
||||||
require 'liquid/drop'
|
require 'liquid/drop'
|
||||||
|
require 'liquid/tablerowloop_drop'
|
||||||
|
require 'liquid/forloop_drop'
|
||||||
require 'liquid/extensions'
|
require 'liquid/extensions'
|
||||||
require 'liquid/errors'
|
require 'liquid/errors'
|
||||||
require 'liquid/interrupts'
|
require 'liquid/interrupts'
|
||||||
@@ -70,7 +74,13 @@ require 'liquid/condition'
|
|||||||
require 'liquid/utils'
|
require 'liquid/utils'
|
||||||
require 'liquid/tokenizer'
|
require 'liquid/tokenizer'
|
||||||
require 'liquid/parse_context'
|
require 'liquid/parse_context'
|
||||||
|
require 'liquid/partial_cache'
|
||||||
|
require 'liquid/usage'
|
||||||
|
|
||||||
# Load all the tags of the standard library
|
# Load all the tags of the standard library
|
||||||
#
|
#
|
||||||
Dir["#{__dir__}/liquid/tags/*.rb"].each { |f| require f }
|
Dir["#{__dir__}/liquid/tags/*.rb"].each { |f| require f }
|
||||||
|
|
||||||
|
# Load all usage tracking
|
||||||
|
#
|
||||||
|
Dir["#{__dir__}/liquid/usages/*.rb"].each { |f| require f }
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
module Liquid
|
module Liquid
|
||||||
class Block < Tag
|
class Block < Tag
|
||||||
|
MAX_DEPTH = 100
|
||||||
|
|
||||||
def initialize(tag_name, markup, options)
|
def initialize(tag_name, markup, options)
|
||||||
super
|
super
|
||||||
@blank = true
|
@blank = true
|
||||||
@@ -11,6 +13,7 @@ module Liquid
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# For backwards compatibility
|
||||||
def render(context)
|
def render(context)
|
||||||
@body.render(context)
|
@body.render(context)
|
||||||
end
|
end
|
||||||
@@ -24,12 +27,12 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
def unknown_tag(tag, _params, _tokens)
|
def unknown_tag(tag, _params, _tokens)
|
||||||
case tag
|
if tag == 'else'.freeze
|
||||||
when 'else'.freeze
|
|
||||||
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_else".freeze,
|
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_else".freeze,
|
||||||
block_name: block_name))
|
block_name: block_name))
|
||||||
when 'end'.freeze
|
elsif tag.start_with?('end'.freeze)
|
||||||
raise SyntaxError.new(parse_context.locale.t("errors.syntax.invalid_delimiter".freeze,
|
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
|
||||||
@@ -48,17 +51,25 @@ module Liquid
|
|||||||
protected
|
protected
|
||||||
|
|
||||||
def parse_body(body, tokens)
|
def parse_body(body, tokens)
|
||||||
body.parse(tokens, parse_context) do |end_tag_name, end_tag_params|
|
if parse_context.depth >= MAX_DEPTH
|
||||||
@blank &&= body.blank?
|
raise StackLevelError, "Nesting too deep".freeze
|
||||||
|
end
|
||||||
|
parse_context.depth += 1
|
||||||
|
begin
|
||||||
|
body.parse(tokens, parse_context) do |end_tag_name, end_tag_params|
|
||||||
|
@blank &&= body.blank?
|
||||||
|
|
||||||
return false if end_tag_name == block_delimiter
|
return false if end_tag_name == block_delimiter
|
||||||
unless end_tag_name
|
unless end_tag_name
|
||||||
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_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
|
||||||
|
# pass it to the current block for special handling or error reporting
|
||||||
|
unknown_tag(end_tag_name, end_tag_params, tokens)
|
||||||
end
|
end
|
||||||
|
ensure
|
||||||
# this tag is not registered with the system
|
parse_context.depth -= 1
|
||||||
# pass it to the current block for special handling or error reporting
|
|
||||||
unknown_tag(end_tag_name, end_tag_params, tokens)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
true
|
true
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
module Liquid
|
module Liquid
|
||||||
class BlockBody
|
class BlockBody
|
||||||
FullToken = /\A#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
|
LiquidTagToken = /\A\s*(\w+)\s*(.*?)\z/o
|
||||||
ContentOfVariable = /\A#{VariableStart}(.*)#{VariableEnd}\z/om
|
FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(\w+)(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
|
||||||
|
ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
|
||||||
|
WhitespaceOrNothing = /\A\s*\z/
|
||||||
TAGSTART = "{%".freeze
|
TAGSTART = "{%".freeze
|
||||||
VARSTART = "{{".freeze
|
VARSTART = "{{".freeze
|
||||||
|
|
||||||
@@ -12,35 +14,34 @@ module Liquid
|
|||||||
@blank = true
|
@blank = true
|
||||||
end
|
end
|
||||||
|
|
||||||
def parse(tokenizer, parse_context)
|
def parse(tokenizer, parse_context, &block)
|
||||||
parse_context.line_number = tokenizer.line_number
|
parse_context.line_number = tokenizer.line_number
|
||||||
|
|
||||||
|
if tokenizer.for_liquid_tag
|
||||||
|
parse_for_liquid_tag(tokenizer, parse_context, &block)
|
||||||
|
else
|
||||||
|
parse_for_document(tokenizer, parse_context, &block)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private def parse_for_liquid_tag(tokenizer, parse_context)
|
||||||
while token = tokenizer.shift
|
while token = tokenizer.shift
|
||||||
unless token.empty?
|
unless token.empty? || token =~ WhitespaceOrNothing
|
||||||
case
|
unless token =~ LiquidTagToken
|
||||||
when token.start_with?(TAGSTART)
|
# line isn't empty but didn't match tag syntax, yield and let the
|
||||||
if token =~ FullToken
|
# caller raise a syntax error
|
||||||
tag_name = $1
|
return yield token, token
|
||||||
markup = $2
|
|
||||||
# fetch the tag from registered blocks
|
|
||||||
if tag = registered_tags[tag_name]
|
|
||||||
new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
|
|
||||||
@blank &&= new_tag.blank?
|
|
||||||
@nodelist << new_tag
|
|
||||||
else
|
|
||||||
# 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)
|
|
||||||
@nodelist << create_variable(token, parse_context)
|
|
||||||
@blank = false
|
|
||||||
else
|
|
||||||
@nodelist << token
|
|
||||||
@blank &&= !!(token =~ /\A\s*\z/)
|
|
||||||
end
|
end
|
||||||
|
tag_name = $1
|
||||||
|
markup = $2
|
||||||
|
unless tag = registered_tags[tag_name]
|
||||||
|
# end parsing if we reach an unknown tag and let the caller decide
|
||||||
|
# determine how to proceed
|
||||||
|
return yield tag_name, markup
|
||||||
|
end
|
||||||
|
new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
|
||||||
|
@blank &&= new_tag.blank?
|
||||||
|
@nodelist << new_tag
|
||||||
end
|
end
|
||||||
parse_context.line_number = tokenizer.line_number
|
parse_context.line_number = tokenizer.line_number
|
||||||
end
|
end
|
||||||
@@ -48,53 +49,121 @@ module Liquid
|
|||||||
yield nil, nil
|
yield nil, nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private def parse_for_document(tokenizer, parse_context, &block)
|
||||||
|
while token = tokenizer.shift
|
||||||
|
next if token.empty?
|
||||||
|
case
|
||||||
|
when token.start_with?(TAGSTART)
|
||||||
|
whitespace_handler(token, parse_context)
|
||||||
|
unless token =~ FullToken
|
||||||
|
raise_missing_tag_terminator(token, parse_context)
|
||||||
|
end
|
||||||
|
tag_name = $2
|
||||||
|
markup = $4
|
||||||
|
|
||||||
|
if parse_context.line_number
|
||||||
|
# newlines inside the tag should increase the line number,
|
||||||
|
# particularly important for multiline {% liquid %} tags
|
||||||
|
parse_context.line_number += $1.count("\n".freeze) + $3.count("\n".freeze)
|
||||||
|
end
|
||||||
|
|
||||||
|
if tag_name == 'liquid'.freeze
|
||||||
|
liquid_tag_tokenizer = Tokenizer.new(markup, line_number: parse_context.line_number, for_liquid_tag: true)
|
||||||
|
next parse_for_liquid_tag(liquid_tag_tokenizer, parse_context, &block)
|
||||||
|
end
|
||||||
|
|
||||||
|
unless tag = registered_tags[tag_name]
|
||||||
|
# end parsing if we reach an unknown tag and let the caller decide
|
||||||
|
# determine how to proceed
|
||||||
|
return yield tag_name, markup
|
||||||
|
end
|
||||||
|
new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
|
||||||
|
@blank &&= new_tag.blank?
|
||||||
|
@nodelist << new_tag
|
||||||
|
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 =~ WhitespaceOrNothing)
|
||||||
|
end
|
||||||
|
parse_context.line_number = tokenizer.line_number
|
||||||
|
end
|
||||||
|
|
||||||
|
yield nil, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def whitespace_handler(token, parse_context)
|
||||||
|
if token[2] == WhitespaceControl
|
||||||
|
previous_token = @nodelist.last
|
||||||
|
if previous_token.is_a? String
|
||||||
|
previous_token.rstrip!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
|
||||||
|
end
|
||||||
|
|
||||||
def blank?
|
def blank?
|
||||||
@blank
|
@blank
|
||||||
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 ::StandardError => e
|
|
||||||
output << context.handle_error(e, token.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.respond_to?(:render) ? node.render(context) : node)
|
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
|
raise MemoryError.new("Memory limits exceeded".freeze)
|
||||||
node_output
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_variable(token, parse_context)
|
def create_variable(token, parse_context)
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ module Liquid
|
|||||||
@@operators
|
@@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)
|
||||||
@@ -41,16 +41,22 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
def evaluate(context = Context.new)
|
def evaluate(context = Context.new)
|
||||||
result = interpret_condition(left, right, operator, context)
|
condition = self
|
||||||
|
result = nil
|
||||||
|
loop do
|
||||||
|
result = interpret_condition(condition.left, condition.right, condition.operator, context)
|
||||||
|
|
||||||
case @child_relation
|
case condition.child_relation
|
||||||
when :or
|
when :or
|
||||||
result || @child_condition.evaluate(context)
|
break if result
|
||||||
when :and
|
when :and
|
||||||
result && @child_condition.evaluate(context)
|
break unless result
|
||||||
else
|
else
|
||||||
result
|
break
|
||||||
|
end
|
||||||
|
condition = condition.child_condition
|
||||||
end
|
end
|
||||||
|
result
|
||||||
end
|
end
|
||||||
|
|
||||||
def or(condition)
|
def or(condition)
|
||||||
@@ -75,6 +81,10 @@ module Liquid
|
|||||||
"#<Condition #{[@left, @operator, @right].compact.join(' '.freeze)}>"
|
"#<Condition #{[@left, @operator, @right].compact.join(' '.freeze)}>"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
attr_reader :child_relation
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def equal_variables(left, right)
|
def equal_variables(left, right)
|
||||||
@@ -110,7 +120,7 @@ module Liquid
|
|||||||
|
|
||||||
if operation.respond_to?(:call)
|
if operation.respond_to?(:call)
|
||||||
operation.call(self, left, right)
|
operation.call(self, left, right)
|
||||||
elsif left.respond_to?(operation) && right.respond_to?(operation)
|
elsif left.respond_to?(operation) && right.respond_to?(operation) && !left.is_a?(Hash) && !right.is_a?(Hash)
|
||||||
begin
|
begin
|
||||||
left.send(operation, right)
|
left.send(operation, right)
|
||||||
rescue ::ArgumentError => e
|
rescue ::ArgumentError => e
|
||||||
@@ -118,6 +128,15 @@ module Liquid
|
|||||||
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
|
||||||
|
|||||||
@@ -12,27 +12,39 @@ module Liquid
|
|||||||
#
|
#
|
||||||
# context['bob'] #=> nil class Context
|
# context['bob'] #=> nil class Context
|
||||||
class Context
|
class Context
|
||||||
attr_reader :scopes, :errors, :registers, :environments, :resource_limits
|
attr_reader :scopes, :errors, :registers, :environments, :resource_limits, :static_registers, :static_environments
|
||||||
attr_accessor :exception_handler, :template_name, :partial
|
attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters
|
||||||
|
|
||||||
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil)
|
# rubocop:disable Metrics/ParameterLists
|
||||||
@environments = [environments].flatten
|
def self.build(environments: {}, outer_scope: {}, registers: {}, rethrow_errors: false, resource_limits: nil, static_registers: {}, static_environments: {})
|
||||||
@scopes = [(outer_scope || {})]
|
new(environments, outer_scope, registers, rethrow_errors, resource_limits, static_registers, static_environments)
|
||||||
@registers = registers
|
end
|
||||||
@errors = []
|
|
||||||
@partial = false
|
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil, static_registers = {}, static_environments = {})
|
||||||
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
|
@environments = [environments].flatten
|
||||||
|
@static_environments = [static_environments].flatten.map(&:freeze).freeze
|
||||||
|
@scopes = [(outer_scope || {})]
|
||||||
|
@registers = registers
|
||||||
|
@static_registers = static_registers.freeze
|
||||||
|
@errors = []
|
||||||
|
@partial = false
|
||||||
|
@strict_variables = false
|
||||||
|
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
|
||||||
|
@base_scope_depth = 0
|
||||||
squash_instance_assigns_with_environments
|
squash_instance_assigns_with_environments
|
||||||
|
|
||||||
@this_stack_used = false
|
@this_stack_used = false
|
||||||
|
|
||||||
|
self.exception_renderer = Template.default_exception_renderer
|
||||||
if rethrow_errors
|
if rethrow_errors
|
||||||
self.exception_handler = ->(e) { raise }
|
self.exception_renderer = ->(e) { raise }
|
||||||
end
|
end
|
||||||
|
|
||||||
@interrupts = []
|
@interrupts = []
|
||||||
@filters = []
|
@filters = []
|
||||||
|
@global_filter = nil
|
||||||
end
|
end
|
||||||
|
# rubocop:enable Metrics/ParameterLists
|
||||||
|
|
||||||
def warnings
|
def warnings
|
||||||
@warnings ||= []
|
@warnings ||= []
|
||||||
@@ -52,6 +64,10 @@ module Liquid
|
|||||||
@strainer = nil
|
@strainer = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def apply_global_filter(obj)
|
||||||
|
global_filter.nil? ? obj : global_filter.call(obj)
|
||||||
|
end
|
||||||
|
|
||||||
# are there any not handled interrupts?
|
# are there any not handled interrupts?
|
||||||
def interrupt?
|
def interrupt?
|
||||||
!@interrupts.empty?
|
!@interrupts.empty?
|
||||||
@@ -68,30 +84,11 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
def handle_error(e, line_number = nil)
|
def handle_error(e, line_number = nil)
|
||||||
if e.is_a?(Liquid::Error)
|
e = internal_error unless e.is_a?(Liquid::Error)
|
||||||
e.template_name ||= template_name
|
e.template_name ||= template_name
|
||||||
e.line_number ||= line_number
|
e.line_number ||= line_number
|
||||||
end
|
|
||||||
|
|
||||||
output = nil
|
|
||||||
|
|
||||||
if exception_handler
|
|
||||||
result = exception_handler.call(e)
|
|
||||||
case result
|
|
||||||
when Exception
|
|
||||||
e = result
|
|
||||||
if e.is_a?(Liquid::Error)
|
|
||||||
e.template_name ||= template_name
|
|
||||||
e.line_number ||= line_number
|
|
||||||
end
|
|
||||||
when String
|
|
||||||
output = result
|
|
||||||
else
|
|
||||||
raise if result
|
|
||||||
end
|
|
||||||
end
|
|
||||||
errors.push(e)
|
errors.push(e)
|
||||||
output || Liquid::Error.render(e)
|
exception_renderer.call(e).to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
def invoke(method, *args)
|
def invoke(method, *args)
|
||||||
@@ -101,7 +98,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 > 100
|
check_overflow
|
||||||
end
|
end
|
||||||
|
|
||||||
# Merge a hash of variables in the current local scope
|
# Merge a hash of variables in the current local scope
|
||||||
@@ -138,6 +135,25 @@ module Liquid
|
|||||||
@this_stack_used = old_stack_used
|
@this_stack_used = old_stack_used
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Creates a new context inheriting resource limits, filters, environment etc.,
|
||||||
|
# but with an isolated scope.
|
||||||
|
def new_isolated_subcontext
|
||||||
|
check_overflow
|
||||||
|
|
||||||
|
Context.build(
|
||||||
|
resource_limits: resource_limits,
|
||||||
|
static_environments: static_environments,
|
||||||
|
static_registers: static_registers
|
||||||
|
).tap do |subcontext|
|
||||||
|
subcontext.base_scope_depth = base_scope_depth + 1
|
||||||
|
subcontext.exception_renderer = exception_renderer
|
||||||
|
subcontext.filters = @filters
|
||||||
|
subcontext.strainer = nil
|
||||||
|
subcontext.errors = errors
|
||||||
|
subcontext.warnings = warnings
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def clear_instance_assigns
|
def clear_instance_assigns
|
||||||
@scopes[0] = {}
|
@scopes[0] = {}
|
||||||
end
|
end
|
||||||
@@ -172,43 +188,76 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Fetches an object starting at the local scope and then moving up the hierachy
|
# Fetches an object starting at the local scope and then moving up the hierachy
|
||||||
def find_variable(key)
|
def find_variable(key, raise_on_not_found: true)
|
||||||
# This was changed from find() to find_index() because this is a very hot
|
# This was changed from find() to find_index() because this is a very hot
|
||||||
# path and find_index() is optimized in MRI to reduce object allocation
|
# path and find_index() is optimized in MRI to reduce object allocation
|
||||||
index = @scopes.find_index { |s| s.key?(key) }
|
index = @scopes.find_index { |s| s.key?(key) }
|
||||||
scope = @scopes[index] if index
|
|
||||||
|
|
||||||
variable = nil
|
variable = if index
|
||||||
|
lookup_and_evaluate(@scopes[index], key, raise_on_not_found: raise_on_not_found)
|
||||||
if scope.nil?
|
else
|
||||||
@environments.each do |e|
|
try_variable_find_in_environments(key, raise_on_not_found: raise_on_not_found)
|
||||||
variable = lookup_and_evaluate(e, key)
|
|
||||||
unless variable.nil?
|
|
||||||
scope = e
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
scope ||= @environments.last || @scopes.last
|
|
||||||
variable ||= lookup_and_evaluate(scope, key)
|
|
||||||
|
|
||||||
variable = variable.to_liquid
|
variable = variable.to_liquid
|
||||||
variable.context = self if variable.respond_to?(:context=)
|
variable.context = self if variable.respond_to?(:context=)
|
||||||
|
|
||||||
variable
|
variable
|
||||||
end
|
end
|
||||||
|
|
||||||
def lookup_and_evaluate(obj, key)
|
def lookup_and_evaluate(obj, key, raise_on_not_found: true)
|
||||||
if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=)
|
if @strict_variables && raise_on_not_found && obj.respond_to?(:key?) && !obj.key?(key)
|
||||||
|
raise Liquid::UndefinedVariable, "undefined variable #{key}"
|
||||||
|
end
|
||||||
|
|
||||||
|
value = obj[key]
|
||||||
|
|
||||||
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
attr_writer :base_scope_depth, :warnings, :errors, :strainer, :filters
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
attr_reader :base_scope_depth
|
||||||
|
|
||||||
|
def try_variable_find_in_environments(key, raise_on_not_found:)
|
||||||
|
@environments.each do |environment|
|
||||||
|
found_variable = lookup_and_evaluate(environment, key, raise_on_not_found: raise_on_not_found)
|
||||||
|
if !found_variable.nil? || @strict_variables && raise_on_not_found
|
||||||
|
return found_variable
|
||||||
|
end
|
||||||
|
end
|
||||||
|
@static_environments.each do |environment|
|
||||||
|
found_variable = lookup_and_evaluate(environment, key, raise_on_not_found: raise_on_not_found)
|
||||||
|
if !found_variable.nil? || @strict_variables && raise_on_not_found
|
||||||
|
return found_variable
|
||||||
|
end
|
||||||
|
end
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_overflow
|
||||||
|
raise StackLevelError, "Nesting too deep".freeze if overflow?
|
||||||
|
end
|
||||||
|
|
||||||
|
def overflow?
|
||||||
|
base_scope_depth + @scopes.length > Block::MAX_DEPTH
|
||||||
|
end
|
||||||
|
|
||||||
|
def internal_error
|
||||||
|
# raise and catch to set backtrace and cause on exception
|
||||||
|
raise Liquid::InternalError, 'internal'
|
||||||
|
rescue Liquid::InternalError => exc
|
||||||
|
exc
|
||||||
|
end
|
||||||
|
|
||||||
def squash_instance_assigns_with_environments
|
def squash_instance_assigns_with_environments
|
||||||
@scopes.last.each_key do |k|
|
@scopes.last.each_key do |k|
|
||||||
@environments.each do |env|
|
@environments.each do |env|
|
||||||
|
|||||||
@@ -18,24 +18,23 @@ module Liquid
|
|||||||
# tmpl = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {%endfor%} ' )
|
# tmpl = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {%endfor%} ' )
|
||||||
# tmpl.render('product' => ProductDrop.new ) # will invoke top_sales query.
|
# tmpl.render('product' => ProductDrop.new ) # will invoke top_sales query.
|
||||||
#
|
#
|
||||||
# Your drop can either implement the methods sans any parameters or implement the before_method(name) method which is a
|
# Your drop can either implement the methods sans any parameters
|
||||||
# catch all.
|
# or implement the liquid_method_missing(name) method which is a catch all.
|
||||||
class Drop
|
class Drop
|
||||||
attr_writer :context
|
attr_writer :context
|
||||||
|
|
||||||
EMPTY_STRING = ''.freeze
|
|
||||||
|
|
||||||
# Catch all for the method
|
# Catch all for the method
|
||||||
def before_method(_method)
|
def liquid_method_missing(method)
|
||||||
nil
|
return nil unless @context && @context.strict_variables
|
||||||
|
raise Liquid::UndefinedDropMethod, "undefined method #{method}"
|
||||||
end
|
end
|
||||||
|
|
||||||
# called by liquid to invoke a drop
|
# called by liquid to invoke a drop
|
||||||
def invoke_drop(method_or_key)
|
def invoke_drop(method_or_key)
|
||||||
if method_or_key && method_or_key != EMPTY_STRING && self.class.invokable?(method_or_key)
|
if self.class.invokable?(method_or_key)
|
||||||
send(method_or_key)
|
send(method_or_key)
|
||||||
else
|
else
|
||||||
before_method(method_or_key)
|
liquid_method_missing(method_or_key)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -63,16 +62,17 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.invokable_methods
|
def self.invokable_methods
|
||||||
unless @invokable_methods
|
@invokable_methods ||= begin
|
||||||
blacklist = Liquid::Drop.public_instance_methods + [:each]
|
blacklist = Liquid::Drop.public_instance_methods + [:each]
|
||||||
|
|
||||||
if include?(Enumerable)
|
if include?(Enumerable)
|
||||||
blacklist += Enumerable.public_instance_methods
|
blacklist += Enumerable.public_instance_methods
|
||||||
blacklist -= [:sort, :count, :first, :min, :max, :include?]
|
blacklist -= [:sort, :count, :first, :min, :max, :include?]
|
||||||
end
|
end
|
||||||
|
|
||||||
whitelist = [:to_liquid] + (public_instance_methods - blacklist)
|
whitelist = [:to_liquid] + (public_instance_methods - blacklist)
|
||||||
@invokable_methods = Set.new(whitelist.map(&:to_s))
|
Set.new(whitelist.map(&:to_s))
|
||||||
end
|
end
|
||||||
@invokable_methods
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -17,14 +17,6 @@ module Liquid
|
|||||||
str
|
str
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.render(e)
|
|
||||||
if e.is_a?(Liquid::Error)
|
|
||||||
e.to_s
|
|
||||||
else
|
|
||||||
"Liquid error: #{e}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def message_prefix
|
def message_prefix
|
||||||
@@ -56,4 +48,9 @@ module Liquid
|
|||||||
MemoryError = Class.new(Error)
|
MemoryError = Class.new(Error)
|
||||||
ZeroDivisionError = Class.new(Error)
|
ZeroDivisionError = Class.new(Error)
|
||||||
FloatDomainError = Class.new(Error)
|
FloatDomainError = Class.new(Error)
|
||||||
|
UndefinedVariable = Class.new(Error)
|
||||||
|
UndefinedDropMethod = Class.new(Error)
|
||||||
|
UndefinedFilter = Class.new(Error)
|
||||||
|
MethodOverrideError = Class.new(Error)
|
||||||
|
InternalError = Class.new(Error)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -19,22 +19,26 @@ module Liquid
|
|||||||
'false'.freeze => false,
|
'false'.freeze => false,
|
||||||
'blank'.freeze => MethodLiteral.new(:blank?, '').freeze,
|
'blank'.freeze => MethodLiteral.new(:blank?, '').freeze,
|
||||||
'empty'.freeze => MethodLiteral.new(:empty?, '').freeze
|
'empty'.freeze => MethodLiteral.new(:empty?, '').freeze
|
||||||
}
|
}.freeze
|
||||||
|
|
||||||
|
SINGLE_QUOTED_STRING = /\A'(.*)'\z/m
|
||||||
|
DOUBLE_QUOTED_STRING = /\A"(.*)"\z/m
|
||||||
|
INTEGERS_REGEX = /\A(-?\d+)\z/
|
||||||
|
FLOATS_REGEX = /\A(-?\d[\d\.]+)\z/
|
||||||
|
RANGES_REGEX = /\A\((\S+)\.\.(\S+)\)\z/
|
||||||
|
|
||||||
def self.parse(markup)
|
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
|
$1
|
||||||
when /\A"(.*)"\z/m # Double quoted strings
|
when INTEGERS_REGEX
|
||||||
$1
|
|
||||||
when /\A(-?\d+)\z/ # Integer and floats
|
|
||||||
$1.to_i
|
$1.to_i
|
||||||
when /\A\((\S+)\.\.(\S+)\)\z/ # Ranges
|
when RANGES_REGEX
|
||||||
RangeLookup.parse($1, $2)
|
RangeLookup.parse($1, $2)
|
||||||
when /\A(-?\d[\d\.]+)\z/ # Floats
|
when FLOATS_REGEX
|
||||||
$1.to_f
|
$1.to_f
|
||||||
else
|
else
|
||||||
VariableLookup.parse(markup)
|
VariableLookup.parse(markup)
|
||||||
|
|||||||
@@ -7,44 +7,56 @@ class String # :nodoc:
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class Array # :nodoc:
|
class Symbol # :nodoc:
|
||||||
|
def to_liquid
|
||||||
|
to_s
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Array # :nodoc:
|
||||||
def to_liquid
|
def to_liquid
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class Hash # :nodoc:
|
class Hash # :nodoc:
|
||||||
def to_liquid
|
def to_liquid
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class Numeric # :nodoc:
|
class Numeric # :nodoc:
|
||||||
def to_liquid
|
def to_liquid
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class Time # :nodoc:
|
class Range # :nodoc:
|
||||||
def to_liquid
|
def to_liquid
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class DateTime < Date # :nodoc:
|
class Time # :nodoc:
|
||||||
def to_liquid
|
def to_liquid
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class Date # :nodoc:
|
class DateTime < Date # :nodoc:
|
||||||
|
def to_liquid
|
||||||
|
self
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Date # :nodoc:
|
||||||
def to_liquid
|
def to_liquid
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class TrueClass
|
class TrueClass
|
||||||
def to_liquid # :nodoc:
|
def to_liquid # :nodoc:
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ module Liquid
|
|||||||
#
|
#
|
||||||
# Example:
|
# Example:
|
||||||
#
|
#
|
||||||
# Liquid::Template.file_system = Liquid::LocalFileSystem.new(template_path)
|
# Liquid::Template.file_system = Liquid::LocalFileSystem.new(template_path)
|
||||||
# liquid = Liquid::Template.parse(template)
|
# liquid = Liquid::Template.parse(template)
|
||||||
#
|
#
|
||||||
# This will parse the template with a LocalFileSystem implementation rooted at 'template_path'.
|
# This will parse the template with a LocalFileSystem implementation rooted at 'template_path'.
|
||||||
class BlankFileSystem
|
class BlankFileSystem
|
||||||
@@ -26,10 +26,10 @@ module Liquid
|
|||||||
#
|
#
|
||||||
# Example:
|
# Example:
|
||||||
#
|
#
|
||||||
# file_system = Liquid::LocalFileSystem.new("/some/path")
|
# file_system = Liquid::LocalFileSystem.new("/some/path")
|
||||||
#
|
#
|
||||||
# file_system.full_path("mypartial") # => "/some/path/_mypartial.liquid"
|
# file_system.full_path("mypartial") # => "/some/path/_mypartial.liquid"
|
||||||
# file_system.full_path("dir/mypartial") # => "/some/path/dir/_mypartial.liquid"
|
# file_system.full_path("dir/mypartial") # => "/some/path/dir/_mypartial.liquid"
|
||||||
#
|
#
|
||||||
# Optionally in the second argument you can specify a custom pattern for template filenames.
|
# Optionally in the second argument you can specify a custom pattern for template filenames.
|
||||||
# The Kernel::sprintf format specification is used.
|
# The Kernel::sprintf format specification is used.
|
||||||
@@ -37,9 +37,9 @@ module Liquid
|
|||||||
#
|
#
|
||||||
# Example:
|
# Example:
|
||||||
#
|
#
|
||||||
# file_system = Liquid::LocalFileSystem.new("/some/path", "%s.html")
|
# file_system = Liquid::LocalFileSystem.new("/some/path", "%s.html")
|
||||||
#
|
#
|
||||||
# file_system.full_path("index") # => "/some/path/index.html"
|
# file_system.full_path("index") # => "/some/path/index.html"
|
||||||
#
|
#
|
||||||
class LocalFileSystem
|
class LocalFileSystem
|
||||||
attr_accessor :root
|
attr_accessor :root
|
||||||
@@ -65,7 +65,7 @@ module Liquid
|
|||||||
File.join(root, @pattern % template_path)
|
File.join(root, @pattern % template_path)
|
||||||
end
|
end
|
||||||
|
|
||||||
raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path) =~ /\A#{File.expand_path(root)}/
|
raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path).start_with?(File.expand_path(root))
|
||||||
|
|
||||||
full_path
|
full_path
|
||||||
end
|
end
|
||||||
|
|||||||
42
lib/liquid/forloop_drop.rb
Normal file
42
lib/liquid/forloop_drop.rb
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
module Liquid
|
||||||
|
class ForloopDrop < Drop
|
||||||
|
def initialize(name, length, parentloop)
|
||||||
|
@name = name
|
||||||
|
@length = length
|
||||||
|
@parentloop = parentloop
|
||||||
|
@index = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
attr_reader :name, :length, :parentloop
|
||||||
|
|
||||||
|
def index
|
||||||
|
@index + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
def index0
|
||||||
|
@index
|
||||||
|
end
|
||||||
|
|
||||||
|
def rindex
|
||||||
|
@length - @index
|
||||||
|
end
|
||||||
|
|
||||||
|
def rindex0
|
||||||
|
@length - @index - 1
|
||||||
|
end
|
||||||
|
|
||||||
|
def first
|
||||||
|
@index == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
def last
|
||||||
|
@index == @length - 1
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def increment!
|
||||||
|
@index += 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -26,7 +26,7 @@ module Liquid
|
|||||||
def interpolate(name, vars)
|
def interpolate(name, vars)
|
||||||
name.gsub(/%\{(\w+)\}/) do
|
name.gsub(/%\{(\w+)\}/) do
|
||||||
# raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym]
|
# raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym]
|
||||||
"#{vars[$1.to_sym]}"
|
(vars[$1.to_sym]).to_s
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -12,23 +12,25 @@ module Liquid
|
|||||||
')'.freeze => :close_round,
|
')'.freeze => :close_round,
|
||||||
'?'.freeze => :question,
|
'?'.freeze => :question,
|
||||||
'-'.freeze => :dash
|
'-'.freeze => :dash
|
||||||
}
|
}.freeze
|
||||||
IDENTIFIER = /[a-zA-Z_][\w-]*\??/
|
IDENTIFIER = /[a-zA-Z_][\w-]*\??/
|
||||||
SINGLE_STRING_LITERAL = /'[^\']*'/
|
SINGLE_STRING_LITERAL = /'[^\']*'/
|
||||||
DOUBLE_STRING_LITERAL = /"[^\"]*"/
|
DOUBLE_STRING_LITERAL = /"[^\"]*"/
|
||||||
NUMBER_LITERAL = /-?\d+(\.\d+)?/
|
NUMBER_LITERAL = /-?\d+(\.\d+)?/
|
||||||
DOTDOT = /\.\./
|
DOTDOT = /\.\./
|
||||||
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains/
|
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/
|
||||||
|
WHITESPACE_OR_NOTHING = /\s*/
|
||||||
|
|
||||||
def initialize(input)
|
def initialize(input)
|
||||||
@ss = StringScanner.new(input.rstrip)
|
@ss = StringScanner.new(input)
|
||||||
end
|
end
|
||||||
|
|
||||||
def tokenize
|
def tokenize
|
||||||
@output = []
|
@output = []
|
||||||
|
|
||||||
until @ss.eos?
|
until @ss.eos?
|
||||||
@ss.skip(/\s*/)
|
@ss.skip(WHITESPACE_OR_NOTHING)
|
||||||
|
break if @ss.eos?
|
||||||
tok = case
|
tok = case
|
||||||
when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t]
|
when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t]
|
||||||
when t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t]
|
when t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t]
|
||||||
|
|||||||
@@ -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}"
|
||||||
@@ -22,3 +22,6 @@
|
|||||||
tag_never_closed: "'%{block_name}' tag was never closed"
|
tag_never_closed: "'%{block_name}' tag was never closed"
|
||||||
meta_syntax_error: "Liquid syntax error: #{e.message}"
|
meta_syntax_error: "Liquid syntax error: #{e.message}"
|
||||||
table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
|
table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
|
||||||
|
render: "Syntax error in tag 'render' - Template name must be a quoted string"
|
||||||
|
argument:
|
||||||
|
include: "Argument error in tag 'include' - Illegal template name"
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
module Liquid
|
module Liquid
|
||||||
class ParseContext
|
class ParseContext
|
||||||
attr_accessor :partial, :locale, :line_number
|
attr_accessor :locale, :line_number, :trim_whitespace, :depth
|
||||||
attr_reader :warnings, :error_mode
|
attr_reader :partial, :warnings, :error_mode
|
||||||
|
|
||||||
def initialize(options = {})
|
def initialize(options = {})
|
||||||
@template_options = options ? options.dup : {}
|
@template_options = options ? options.dup : {}
|
||||||
@locale = @template_options[:locale] ||= I18n.new
|
@locale = @template_options[:locale] ||= I18n.new
|
||||||
@warnings = []
|
@warnings = []
|
||||||
|
self.depth = 0
|
||||||
self.partial = false
|
self.partial = false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
42
lib/liquid/parse_tree_visitor.rb
Normal file
42
lib/liquid/parse_tree_visitor.rb
Normal 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
|
||||||
@@ -44,11 +44,14 @@ module Liquid
|
|||||||
tok[0] == type
|
tok[0] == type
|
||||||
end
|
end
|
||||||
|
|
||||||
|
SINGLE_TOKEN_EXPRESSION_TYPES = [:string, :number].freeze
|
||||||
|
private_constant :SINGLE_TOKEN_EXPRESSION_TYPES
|
||||||
|
|
||||||
def expression
|
def expression
|
||||||
token = @tokens[@p]
|
token = @tokens[@p]
|
||||||
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
|
||||||
@@ -75,7 +78,7 @@ module Liquid
|
|||||||
|
|
||||||
def variable_signature
|
def variable_signature
|
||||||
str = consume(:id)
|
str = consume(:id)
|
||||||
if look(:open_square)
|
while look(:open_square)
|
||||||
str << consume
|
str << consume
|
||||||
str << expression
|
str << expression
|
||||||
str << consume(:close_square)
|
str << consume(:close_square)
|
||||||
|
|||||||
18
lib/liquid/partial_cache.rb
Normal file
18
lib/liquid/partial_cache.rb
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
module Liquid
|
||||||
|
class PartialCache
|
||||||
|
def self.load(template_name, context:, parse_context:)
|
||||||
|
cached_partials = (context.registers[:cached_partials] ||= {})
|
||||||
|
cached = cached_partials[template_name]
|
||||||
|
return cached if cached
|
||||||
|
|
||||||
|
file_system = (context.registers[:file_system] ||= Liquid::Template.file_system)
|
||||||
|
source = file_system.read_template_file(template_name)
|
||||||
|
parse_context.partial = true
|
||||||
|
|
||||||
|
partial = Liquid::Template.parse(source, parse_context)
|
||||||
|
cached_partials[template_name] = partial
|
||||||
|
ensure
|
||||||
|
parse_context.partial = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
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 +11,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
|
||||||
|
|||||||
@@ -9,8 +9,14 @@ module Liquid
|
|||||||
'<'.freeze => '<'.freeze,
|
'<'.freeze => '<'.freeze,
|
||||||
'"'.freeze => '"'.freeze,
|
'"'.freeze => '"'.freeze,
|
||||||
"'".freeze => '''.freeze
|
"'".freeze => '''.freeze
|
||||||
}
|
}.freeze
|
||||||
HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/
|
HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/
|
||||||
|
STRIP_HTML_BLOCKS = Regexp.union(
|
||||||
|
/<script.*?<\/script>/m,
|
||||||
|
/<!--.*?-->/m,
|
||||||
|
/<style.*?<\/style>/m
|
||||||
|
)
|
||||||
|
STRIP_HTML_TAGS = /<.*?>/m
|
||||||
|
|
||||||
# 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)
|
||||||
@@ -33,7 +39,7 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
def escape(input)
|
def escape(input)
|
||||||
CGI.escapeHTML(input).untaint
|
CGI.escapeHTML(input.to_s).untaint unless input.nil?
|
||||||
end
|
end
|
||||||
alias_method :h, :escape
|
alias_method :h, :escape
|
||||||
|
|
||||||
@@ -42,7 +48,16 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
def url_encode(input)
|
def url_encode(input)
|
||||||
CGI.escape(input) rescue input
|
CGI.escape(input.to_s) unless input.nil?
|
||||||
|
end
|
||||||
|
|
||||||
|
def url_decode(input)
|
||||||
|
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)
|
||||||
@@ -59,10 +74,12 @@ module Liquid
|
|||||||
# 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 = "...".freeze)
|
||||||
return if input.nil?
|
return if input.nil?
|
||||||
|
input_str = input.to_s
|
||||||
length = Utils.to_integer(length)
|
length = Utils.to_integer(length)
|
||||||
l = length - truncate_string.length
|
truncate_string_str = truncate_string.to_s
|
||||||
|
l = length - truncate_string_str.length
|
||||||
l = 0 if l < 0
|
l = 0 if l < 0
|
||||||
input.length > length ? input[0...l] + truncate_string : input
|
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 = "...".freeze)
|
||||||
@@ -71,7 +88,7 @@ module Liquid
|
|||||||
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 : input
|
wordlist.length > l ? wordlist[0..l].join(" ".freeze).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.
|
||||||
@@ -80,7 +97,7 @@ module Liquid
|
|||||||
# <div class="summary">{{ post | split '//' | first }}</div>
|
# <div class="summary">{{ post | split '//' | first }}</div>
|
||||||
#
|
#
|
||||||
def split(input, pattern)
|
def split(input, pattern)
|
||||||
input.to_s.split(pattern)
|
input.to_s.split(pattern.to_s)
|
||||||
end
|
end
|
||||||
|
|
||||||
def strip(input)
|
def strip(input)
|
||||||
@@ -97,7 +114,9 @@ module Liquid
|
|||||||
|
|
||||||
def strip_html(input)
|
def strip_html(input)
|
||||||
empty = ''.freeze
|
empty = ''.freeze
|
||||||
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
|
||||||
@@ -114,12 +133,19 @@ 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
|
ary.sort do |a, b|
|
||||||
elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
|
nil_safe_compare(a, b)
|
||||||
ary.sort { |a, b| a[property] <=> b[property] }
|
end
|
||||||
elsif ary.first.respond_to?(property)
|
elsif ary.all? { |el| el.respond_to?(:[]) }
|
||||||
ary.sort { |a, b| a.send(property) <=> b.send(property) }
|
begin
|
||||||
|
ary.sort { |a, b| nil_safe_compare(a[property], b[property]) }
|
||||||
|
rescue TypeError
|
||||||
|
raise_property_error(property)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -128,12 +154,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.first.respond_to?(:[]) && !ary.first[property].nil?
|
nil_safe_casecmp(a, b)
|
||||||
ary.sort { |a, b| a[property].casecmp(b[property]) }
|
end
|
||||||
elsif ary.first.respond_to?(property)
|
elsif ary.all? { |el| el.respond_to?(:[]) }
|
||||||
ary.sort { |a, b| a.send(property).casecmp(b.send(property)) }
|
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?(:[]) && target_value.nil?
|
||||||
|
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
|
||||||
|
|
||||||
@@ -144,8 +198,14 @@ module Liquid
|
|||||||
|
|
||||||
if property.nil?
|
if property.nil?
|
||||||
ary.uniq
|
ary.uniq
|
||||||
|
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
|
||||||
|
|
||||||
@@ -163,9 +223,12 @@ module Liquid
|
|||||||
if property == "to_liquid".freeze
|
if property == "to_liquid".freeze
|
||||||
e
|
e
|
||||||
elsif e.respond_to?(:[])
|
elsif e.respond_to?(:[])
|
||||||
e[property]
|
r = e[property]
|
||||||
|
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
|
||||||
@@ -175,10 +238,14 @@ module Liquid
|
|||||||
|
|
||||||
if property.nil?
|
if property.nil?
|
||||||
ary.compact
|
ary.compact
|
||||||
|
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
|
||||||
elsif ary.first.respond_to?(property)
|
ary.reject { |a| a[property].nil? }
|
||||||
ary.reject { |a| a.send(property).nil? }
|
rescue TypeError
|
||||||
|
raise_property_error(property)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -208,6 +275,9 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
def concat(input, array)
|
def concat(input, array)
|
||||||
|
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
|
||||||
|
|
||||||
@@ -278,6 +348,12 @@ module Liquid
|
|||||||
array.last if array.respond_to?(:last)
|
array.last if array.respond_to?(:last)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# absolute value
|
||||||
|
def abs(input)
|
||||||
|
result = Utils.to_number(input).abs
|
||||||
|
result.is_a?(BigDecimal) ? result.to_f : result
|
||||||
|
end
|
||||||
|
|
||||||
# addition
|
# addition
|
||||||
def plus(input, operand)
|
def plus(input, operand)
|
||||||
apply_operation(input, operand, :+)
|
apply_operation(input, operand, :+)
|
||||||
@@ -327,18 +403,57 @@ 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)
|
||||||
is_blank = input.respond_to?(:empty?) ? input.empty? : !input
|
min_value = Utils.to_number(n)
|
||||||
is_blank ? default_value : input
|
|
||||||
|
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 = ''.freeze)
|
||||||
|
if !input || input.respond_to?(:empty?) && input.empty?
|
||||||
|
default_value
|
||||||
|
else
|
||||||
|
input
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def raise_property_error(property)
|
||||||
|
raise Liquid::ArgumentError.new("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
|
||||||
|
|
||||||
@@ -355,11 +470,11 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
def join(glue)
|
def join(glue)
|
||||||
to_a.join(glue)
|
to_a.join(glue.to_s)
|
||||||
end
|
end
|
||||||
|
|
||||||
def concat(args)
|
def concat(args)
|
||||||
to_a.concat args
|
to_a.concat(args)
|
||||||
end
|
end
|
||||||
|
|
||||||
def reverse
|
def reverse
|
||||||
@@ -374,6 +489,11 @@ module Liquid
|
|||||||
to_a.compact
|
to_a.compact
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def empty?
|
||||||
|
@input.each { return false }
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
def each
|
def each
|
||||||
@input.each do |e|
|
@input.each do |e|
|
||||||
yield(e.respond_to?(:to_liquid) ? e.to_liquid : e)
|
yield(e.respond_to?(:to_liquid) ? e.to_liquid : e)
|
||||||
|
|||||||
@@ -26,14 +26,20 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.add_filter(filter)
|
def self.add_filter(filter)
|
||||||
raise ArgumentError, "Expected module but got: #{f.class}" unless filter.is_a?(Module)
|
raise ArgumentError, "Expected module but got: #{filter.class}" unless filter.is_a?(Module)
|
||||||
unless self.class.include?(filter)
|
unless self.include?(filter)
|
||||||
send(:include, filter)
|
invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }
|
||||||
@filter_methods.merge(filter.public_instance_methods.map(&:to_s))
|
if invokable_non_public_methods.any?
|
||||||
|
raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"
|
||||||
|
else
|
||||||
|
send(:include, filter)
|
||||||
|
@filter_methods.merge(filter.public_instance_methods.map(&:to_s))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.global_filter(filter)
|
def self.global_filter(filter)
|
||||||
|
@@strainer_class_cache.clear
|
||||||
@@global_strainer.add_filter(filter)
|
@@global_strainer.add_filter(filter)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -48,11 +54,13 @@ module Liquid
|
|||||||
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
|
||||||
|
raise Liquid::UndefinedFilter, "undefined filter #{method}"
|
||||||
else
|
else
|
||||||
args.first
|
args.first
|
||||||
end
|
end
|
||||||
rescue ::ArgumentError => e
|
rescue ::ArgumentError => e
|
||||||
raise Liquid::ArgumentError.new(e.message)
|
raise Liquid::ArgumentError, e.message, e.backtrace
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
62
lib/liquid/tablerowloop_drop.rb
Normal file
62
lib/liquid/tablerowloop_drop.rb
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
module Liquid
|
||||||
|
class TablerowloopDrop < Drop
|
||||||
|
def initialize(length, cols)
|
||||||
|
@length = length
|
||||||
|
@row = 1
|
||||||
|
@col = 1
|
||||||
|
@cols = cols
|
||||||
|
@index = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
attr_reader :length, :col, :row
|
||||||
|
|
||||||
|
def index
|
||||||
|
@index + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
def index0
|
||||||
|
@index
|
||||||
|
end
|
||||||
|
|
||||||
|
def col0
|
||||||
|
@col - 1
|
||||||
|
end
|
||||||
|
|
||||||
|
def rindex
|
||||||
|
@length - @index
|
||||||
|
end
|
||||||
|
|
||||||
|
def rindex0
|
||||||
|
@length - @index - 1
|
||||||
|
end
|
||||||
|
|
||||||
|
def first
|
||||||
|
@index == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
def last
|
||||||
|
@index == @length - 1
|
||||||
|
end
|
||||||
|
|
||||||
|
def col_first
|
||||||
|
@col == 1
|
||||||
|
end
|
||||||
|
|
||||||
|
def col_last
|
||||||
|
@col == @cols
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def increment!
|
||||||
|
@index += 1
|
||||||
|
|
||||||
|
if @col == @cols
|
||||||
|
@col = 1
|
||||||
|
@row += 1
|
||||||
|
else
|
||||||
|
@col += 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -5,8 +5,8 @@ module Liquid
|
|||||||
include ParserSwitching
|
include ParserSwitching
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def parse(tag_name, markup, tokenizer, options)
|
def parse(tag_name, markup, tokenizer, parse_context)
|
||||||
tag = new(tag_name, markup, options)
|
tag = new(tag_name, markup, parse_context)
|
||||||
tag.parse(tokenizer)
|
tag.parse(tokenizer)
|
||||||
tag
|
tag
|
||||||
end
|
end
|
||||||
@@ -36,6 +36,14 @@ module Liquid
|
|||||||
''.freeze
|
''.freeze
|
||||||
end
|
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
|
||||||
|
|
||||||
def blank?
|
def blank?
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -10,29 +10,53 @@ module Liquid
|
|||||||
class Assign < Tag
|
class Assign < Tag
|
||||||
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
|
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
|
||||||
|
|
||||||
|
def self.syntax_error_translation_key
|
||||||
|
"errors.syntax.assign".freeze
|
||||||
|
end
|
||||||
|
|
||||||
|
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 = $1
|
||||||
@from = Variable.new($2, options)
|
@from = Variable.new($2, options)
|
||||||
else
|
else
|
||||||
raise SyntaxError.new options[:locale].t("errors.syntax.assign".freeze)
|
raise SyntaxError.new(options[:locale].t(self.class.syntax_error_translation_key))
|
||||||
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)
|
||||||
inc = val.instance_of?(String) || val.instance_of?(Array) || val.instance_of?(Hash) ? val.length : 1
|
output
|
||||||
context.resource_limits.assign_score += inc
|
|
||||||
|
|
||||||
''.freeze
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def blank?
|
def blank?
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def assign_score_of(val)
|
||||||
|
if val.instance_of?(String)
|
||||||
|
val.bytesize
|
||||||
|
elsif val.instance_of?(Array) || val.instance_of?(Hash)
|
||||||
|
sum = 1
|
||||||
|
# Uses #each to avoid extra allocations.
|
||||||
|
val.each { |child| sum += assign_score_of(child) }
|
||||||
|
sum
|
||||||
|
else
|
||||||
|
1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||||
|
def children
|
||||||
|
[@node.from]
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
Template.register_tag('assign'.freeze, Assign)
|
Template.register_tag('assign'.freeze, Assign)
|
||||||
|
|||||||
@@ -22,11 +22,12 @@ module Liquid
|
|||||||
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?
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ module Liquid
|
|||||||
Syntax = /(#{QuotedFragment})/o
|
Syntax = /(#{QuotedFragment})/o
|
||||||
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om
|
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om
|
||||||
|
|
||||||
|
attr_reader :blocks, :left
|
||||||
|
|
||||||
def initialize(tag_name, markup, options)
|
def initialize(tag_name, markup, options)
|
||||||
super
|
super
|
||||||
@blocks = []
|
@blocks = []
|
||||||
@@ -36,21 +38,21 @@ module Liquid
|
|||||||
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
|
||||||
@@ -80,6 +82,12 @@ module Liquid
|
|||||||
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'.freeze, Case)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
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)
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ module Liquid
|
|||||||
SimpleSyntax = /\A#{QuotedFragment}+/o
|
SimpleSyntax = /\A#{QuotedFragment}+/o
|
||||||
NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om
|
NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om
|
||||||
|
|
||||||
|
attr_reader :variables
|
||||||
|
|
||||||
def initialize(tag_name, markup, options)
|
def initialize(tag_name, markup, options)
|
||||||
super
|
super
|
||||||
case markup
|
case markup
|
||||||
@@ -29,18 +31,29 @@ module Liquid
|
|||||||
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
|
||||||
@@ -51,6 +64,12 @@ module Liquid
|
|||||||
$1 ? Expression.parse($1) : nil
|
$1 ? Expression.parse($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)
|
||||||
|
|||||||
@@ -23,11 +23,12 @@ 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
|
||||||
|
|
||||||
|
|||||||
24
lib/liquid/tags/echo.rb
Normal file
24
lib/liquid/tags/echo.rb
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
module Liquid
|
||||||
|
# Echo outputs an expression
|
||||||
|
#
|
||||||
|
# {% echo monkey %}
|
||||||
|
# {% echo user.name %}
|
||||||
|
#
|
||||||
|
# This is identical to variable output syntax, like {{ foo }}, but works
|
||||||
|
# inside {% liquid %} tags. The full syntax is supported, including filters:
|
||||||
|
#
|
||||||
|
# {% echo user | link %}
|
||||||
|
#
|
||||||
|
class Echo < Tag
|
||||||
|
def initialize(tag_name, markup, parse_context)
|
||||||
|
super
|
||||||
|
@variable = Variable.new(markup, parse_context)
|
||||||
|
end
|
||||||
|
|
||||||
|
def render(context)
|
||||||
|
@variable.render_to_output_buffer(context, '')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Template.register_tag('echo'.freeze, Echo)
|
||||||
|
end
|
||||||
@@ -23,7 +23,7 @@ module Liquid
|
|||||||
# {{ item.name }}
|
# {{ item.name }}
|
||||||
# {% end %}
|
# {% end %}
|
||||||
#
|
#
|
||||||
# To reverse the for loop simply use {% for item in collection reversed %}
|
# To reverse the for loop simply use {% for item in collection reversed %} (note that the flag's spelling is different to the filter `reverse`)
|
||||||
#
|
#
|
||||||
# == Available variables:
|
# == Available variables:
|
||||||
#
|
#
|
||||||
@@ -46,10 +46,14 @@ module Liquid
|
|||||||
class For < Block
|
class For < Block
|
||||||
Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
|
Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
|
||||||
|
|
||||||
|
attr_reader :collection_name, :variable_name, :limit, :from
|
||||||
|
|
||||||
def initialize(tag_name, markup, options)
|
def initialize(tag_name, markup, options)
|
||||||
super
|
super
|
||||||
|
@from = @limit = nil
|
||||||
parse_with_selected_parser(markup)
|
parse_with_selected_parser(markup)
|
||||||
@for_block = BlockBody.new
|
@for_block = BlockBody.new
|
||||||
|
@else_block = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def parse(tokens)
|
def parse(tokens)
|
||||||
@@ -66,70 +70,16 @@ module Liquid
|
|||||||
@else_block = BlockBody.new
|
@else_block = BlockBody.new
|
||||||
end
|
end
|
||||||
|
|
||||||
def render(context)
|
def render_to_output_buffer(context, output)
|
||||||
for_offsets = context.registers[:for] ||= Hash.new(0)
|
segment = collection_segment(context)
|
||||||
for_stack = context.registers[:for_stack] ||= []
|
|
||||||
|
|
||||||
parent_loop = for_stack.last
|
if segment.empty?
|
||||||
for_stack.push(nil)
|
render_else(context, output)
|
||||||
|
|
||||||
collection = context.evaluate(@collection_name)
|
|
||||||
collection = collection.to_a if collection.is_a?(Range)
|
|
||||||
|
|
||||||
from = if @from == :continue
|
|
||||||
for_offsets[@name].to_i
|
|
||||||
else
|
else
|
||||||
context.evaluate(@from).to_i
|
render_segment(context, output, segment)
|
||||||
end
|
end
|
||||||
|
|
||||||
limit = context.evaluate(@limit)
|
output
|
||||||
to = limit ? limit.to_i + from : nil
|
|
||||||
|
|
||||||
segment = Utils.slice_collection(collection, from, to)
|
|
||||||
|
|
||||||
return render_else(context) if segment.empty?
|
|
||||||
|
|
||||||
segment.reverse! if @reversed
|
|
||||||
|
|
||||||
result = ''
|
|
||||||
|
|
||||||
length = segment.length
|
|
||||||
|
|
||||||
# Store our progress through the collection for the continue flag
|
|
||||||
for_offsets[@name] = from + segment.length
|
|
||||||
|
|
||||||
context.stack do
|
|
||||||
segment.each_with_index do |item, index|
|
|
||||||
context[@variable_name] = item
|
|
||||||
loop_vars = {
|
|
||||||
'name'.freeze => @name,
|
|
||||||
'length'.freeze => length,
|
|
||||||
'index'.freeze => index + 1,
|
|
||||||
'index0'.freeze => index,
|
|
||||||
'rindex'.freeze => length - index,
|
|
||||||
'rindex0'.freeze => length - index - 1,
|
|
||||||
'first'.freeze => (index == 0),
|
|
||||||
'last'.freeze => (index == length - 1),
|
|
||||||
'parentloop'.freeze => parent_loop
|
|
||||||
}
|
|
||||||
|
|
||||||
context['forloop'.freeze] = loop_vars
|
|
||||||
for_stack[-1] = loop_vars
|
|
||||||
|
|
||||||
result << @for_block.render(context)
|
|
||||||
|
|
||||||
# Handle any interrupts if they exist.
|
|
||||||
if context.interrupt?
|
|
||||||
interrupt = context.pop_interrupt
|
|
||||||
break if interrupt.is_a? BreakInterrupt
|
|
||||||
next if interrupt.is_a? ContinueInterrupt
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
result
|
|
||||||
ensure
|
|
||||||
for_stack.pop
|
|
||||||
end
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
@@ -152,7 +102,7 @@ module Liquid
|
|||||||
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.new(options[:locale].t("errors.syntax.for_invalid_in".freeze)) unless p.id?('in'.freeze)
|
||||||
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)
|
||||||
@@ -170,6 +120,70 @@ module Liquid
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def collection_segment(context)
|
||||||
|
offsets = context.registers[:for] ||= {}
|
||||||
|
|
||||||
|
from = if @from == :continue
|
||||||
|
offsets[@name].to_i
|
||||||
|
else
|
||||||
|
from_value = context.evaluate(@from)
|
||||||
|
if from_value.nil?
|
||||||
|
0
|
||||||
|
else
|
||||||
|
Utils.to_integer(from_value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
collection = context.evaluate(@collection_name)
|
||||||
|
collection = collection.to_a if collection.is_a?(Range)
|
||||||
|
|
||||||
|
limit_value = context.evaluate(@limit)
|
||||||
|
to = if limit_value.nil?
|
||||||
|
nil
|
||||||
|
else
|
||||||
|
Utils.to_integer(limit_value) + from
|
||||||
|
end
|
||||||
|
|
||||||
|
segment = Utils.slice_collection(collection, from, to)
|
||||||
|
segment.reverse! if @reversed
|
||||||
|
|
||||||
|
offsets[@name] = from + segment.length
|
||||||
|
|
||||||
|
segment
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_segment(context, output, segment)
|
||||||
|
for_stack = context.registers[:for_stack] ||= []
|
||||||
|
length = segment.length
|
||||||
|
|
||||||
|
context.stack do
|
||||||
|
loop_vars = Liquid::ForloopDrop.new(@name, length, for_stack[-1])
|
||||||
|
|
||||||
|
for_stack.push(loop_vars)
|
||||||
|
|
||||||
|
begin
|
||||||
|
context['forloop'.freeze] = loop_vars
|
||||||
|
|
||||||
|
segment.each do |item|
|
||||||
|
context[@variable_name] = item
|
||||||
|
@for_block.render_to_output_buffer(context, output)
|
||||||
|
loop_vars.send(:increment!)
|
||||||
|
|
||||||
|
# Handle any interrupts if they exist.
|
||||||
|
if context.interrupt?
|
||||||
|
interrupt = context.pop_interrupt
|
||||||
|
break if interrupt.is_a? BreakInterrupt
|
||||||
|
next if interrupt.is_a? ContinueInterrupt
|
||||||
|
end
|
||||||
|
end
|
||||||
|
ensure
|
||||||
|
for_stack.pop
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
output
|
||||||
|
end
|
||||||
|
|
||||||
def set_attribute(key, expr)
|
def set_attribute(key, expr)
|
||||||
case key
|
case key
|
||||||
when 'offset'.freeze
|
when 'offset'.freeze
|
||||||
@@ -183,8 +197,18 @@ module Liquid
|
|||||||
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
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ module Liquid
|
|||||||
class If < Block
|
class If < Block
|
||||||
Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o
|
Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o
|
||||||
ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o
|
ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o
|
||||||
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
|
||||||
@@ -20,15 +22,15 @@ module Liquid
|
|||||||
push_block('if'.freeze, markup)
|
push_block('if'.freeze, markup)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def nodelist
|
||||||
|
@blocks.map(&:attachment)
|
||||||
|
end
|
||||||
|
|
||||||
def parse(tokens)
|
def parse(tokens)
|
||||||
while parse_body(@blocks.last.attachment, tokens)
|
while parse_body(@blocks.last.attachment, tokens)
|
||||||
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 ['elsif'.freeze, 'else'.freeze].include?(tag)
|
||||||
push_block(tag, markup)
|
push_block(tag, markup)
|
||||||
@@ -37,15 +39,16 @@ module Liquid
|
|||||||
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)
|
if block.evaluate(context)
|
||||||
return block.attachment.render(context)
|
return block.attachment.render_to_output_buffer(context, output)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
''.freeze
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
output
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -83,17 +86,20 @@ module Liquid
|
|||||||
|
|
||||||
def strict_parse(markup)
|
def strict_parse(markup)
|
||||||
p = Parser.new(markup)
|
p = Parser.new(markup)
|
||||||
condition = parse_binary_comparison(p)
|
condition = parse_binary_comparisons(p)
|
||||||
p.consume(:end_of_string)
|
p.consume(:end_of_string)
|
||||||
condition
|
condition
|
||||||
end
|
end
|
||||||
|
|
||||||
def parse_binary_comparison(p)
|
def parse_binary_comparisons(p)
|
||||||
condition = parse_comparison(p)
|
condition = parse_comparison(p)
|
||||||
if op = (p.id?('and'.freeze) || p.id?('or'.freeze))
|
first_condition = condition
|
||||||
condition.send(op, parse_binary_comparison(p))
|
while op = (p.id?('and'.freeze) || p.id?('or'.freeze))
|
||||||
|
child_condition = parse_comparison(p)
|
||||||
|
condition.send(op, child_condition)
|
||||||
|
condition = child_condition
|
||||||
end
|
end
|
||||||
condition
|
first_condition
|
||||||
end
|
end
|
||||||
|
|
||||||
def parse_comparison(p)
|
def parse_comparison(p)
|
||||||
@@ -105,6 +111,12 @@ module Liquid
|
|||||||
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'.freeze, If)
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
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
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ module Liquid
|
|||||||
class Include < Tag
|
class Include < Tag
|
||||||
Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/o
|
Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/o
|
||||||
|
|
||||||
|
attr_reader :template_name_expr, :variable_name_expr, :attributes
|
||||||
|
|
||||||
def initialize(tag_name, markup, options)
|
def initialize(tag_name, markup, options)
|
||||||
super
|
super
|
||||||
|
|
||||||
@@ -40,16 +42,22 @@ module Liquid
|
|||||||
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)
|
||||||
partial = load_cached_partial(template_name, context)
|
raise ArgumentError.new(options[:locale].t("errors.argument.include")) unless template_name
|
||||||
|
|
||||||
|
partial = PartialCache.load(
|
||||||
|
template_name,
|
||||||
|
context: context,
|
||||||
|
parse_context: parse_context
|
||||||
|
)
|
||||||
|
|
||||||
context_variable_name = template_name.split('/'.freeze).last
|
context_variable_name = template_name.split('/'.freeze).last
|
||||||
|
|
||||||
variable = if @variable_name_expr
|
variable = if @variable_name_expr
|
||||||
context.evaluate(@variable_name_expr)
|
context.evaluate(@variable_name_expr)
|
||||||
else
|
else
|
||||||
context.find_variable(template_name)
|
context.find_variable(template_name, raise_on_not_found: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
old_template_name = context.template_name
|
old_template_name = context.template_name
|
||||||
@@ -63,48 +71,33 @@ 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
|
||||||
end
|
|
||||||
|
|
||||||
private
|
output
|
||||||
|
end
|
||||||
|
|
||||||
alias_method :parse_context, :options
|
alias_method :parse_context, :options
|
||||||
private :parse_context
|
private :parse_context
|
||||||
|
|
||||||
def load_cached_partial(template_name, context)
|
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||||
cached_partials = context.registers[:cached_partials] || {}
|
def children
|
||||||
|
[
|
||||||
if cached = cached_partials[template_name]
|
@node.template_name_expr,
|
||||||
return cached
|
@node.variable_name_expr
|
||||||
|
] + @node.attributes.values
|
||||||
end
|
end
|
||||||
source = read_template_from_file_system(context)
|
|
||||||
begin
|
|
||||||
parse_context.partial = true
|
|
||||||
partial = Liquid::Template.parse(source, parse_context)
|
|
||||||
ensure
|
|
||||||
parse_context.partial = false
|
|
||||||
end
|
|
||||||
cached_partials[template_name] = partial
|
|
||||||
context.registers[:cached_partials] = cached_partials
|
|
||||||
partial
|
|
||||||
end
|
|
||||||
|
|
||||||
def read_template_from_file_system(context)
|
|
||||||
file_system = context.registers[:file_system] || Liquid::Template.file_system
|
|
||||||
|
|
||||||
file_system.read_template_file(context.evaluate(@template_name_expr))
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -20,10 +20,12 @@ 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
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,7 @@ module Liquid
|
|||||||
def initialize(tag_name, markup, parse_context)
|
def initialize(tag_name, markup, parse_context)
|
||||||
super
|
super
|
||||||
|
|
||||||
unless markup =~ Syntax
|
ensure_valid_markup(tag_name, markup, parse_context)
|
||||||
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_unexpected_args".freeze, tag: tag_name))
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def parse(tokens)
|
def parse(tokens)
|
||||||
@@ -24,8 +22,9 @@ module Liquid
|
|||||||
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
|
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, 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
|
||||||
@@ -35,6 +34,14 @@ module Liquid
|
|||||||
def blank?
|
def blank?
|
||||||
@body.empty?
|
@body.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def ensure_valid_markup(tag_name, markup, parse_context)
|
||||||
|
unless markup =~ Syntax
|
||||||
|
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_unexpected_args".freeze, tag: tag_name))
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
Template.register_tag('raw'.freeze, Raw)
|
Template.register_tag('raw'.freeze, Raw)
|
||||||
|
|||||||
54
lib/liquid/tags/render.rb
Normal file
54
lib/liquid/tags/render.rb
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
module Liquid
|
||||||
|
class Render < Tag
|
||||||
|
Syntax = /(#{QuotedString})#{QuotedFragment}*/o
|
||||||
|
|
||||||
|
attr_reader :template_name_expr, :attributes
|
||||||
|
|
||||||
|
def initialize(tag_name, markup, options)
|
||||||
|
super
|
||||||
|
|
||||||
|
raise SyntaxError.new(options[:locale].t("errors.syntax.render".freeze)) unless markup =~ Syntax
|
||||||
|
|
||||||
|
template_name = $1
|
||||||
|
|
||||||
|
@template_name_expr = Expression.parse(template_name)
|
||||||
|
|
||||||
|
@attributes = {}
|
||||||
|
markup.scan(TagAttributes) do |key, value|
|
||||||
|
@attributes[key] = Expression.parse(value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_to_output_buffer(context, output)
|
||||||
|
# Though we evaluate this here we will only ever parse it as a string literal.
|
||||||
|
template_name = context.evaluate(@template_name_expr)
|
||||||
|
raise ArgumentError.new(options[:locale].t("errors.argument.include")) unless template_name
|
||||||
|
|
||||||
|
partial = PartialCache.load(
|
||||||
|
template_name,
|
||||||
|
context: context,
|
||||||
|
parse_context: parse_context
|
||||||
|
)
|
||||||
|
|
||||||
|
inner_context = context.new_isolated_subcontext
|
||||||
|
inner_context.template_name = template_name
|
||||||
|
inner_context.partial = true
|
||||||
|
@attributes.each do |key, value|
|
||||||
|
inner_context[key] = context.evaluate(value)
|
||||||
|
end
|
||||||
|
partial.render_to_output_buffer(inner_context, output)
|
||||||
|
|
||||||
|
output
|
||||||
|
end
|
||||||
|
|
||||||
|
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||||
|
def children
|
||||||
|
[
|
||||||
|
@node.template_name_expr,
|
||||||
|
] + @node.attributes.values
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Template.register_tag('render'.freeze, Render)
|
||||||
|
end
|
||||||
@@ -2,6 +2,8 @@ module Liquid
|
|||||||
class TableRow < Block
|
class TableRow < Block
|
||||||
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
|
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
|
||||||
|
|
||||||
|
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
|
||||||
@@ -16,7 +18,7 @@ module Liquid
|
|||||||
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) or return ''.freeze
|
||||||
|
|
||||||
from = @attributes.key?('offset'.freeze) ? context.evaluate(@attributes['offset'.freeze]).to_i : 0
|
from = @attributes.key?('offset'.freeze) ? context.evaluate(@attributes['offset'.freeze]).to_i : 0
|
||||||
@@ -28,40 +30,34 @@ module Liquid
|
|||||||
|
|
||||||
cols = context.evaluate(@attributes['cols'.freeze]).to_i
|
cols = context.evaluate(@attributes['cols'.freeze]).to_i
|
||||||
|
|
||||||
row = 1
|
output << "<tr class=\"row1\">\n"
|
||||||
col = 0
|
|
||||||
|
|
||||||
result = "<tr class=\"row1\">\n"
|
|
||||||
context.stack do
|
context.stack do
|
||||||
collection.each_with_index do |item, index|
|
tablerowloop = Liquid::TablerowloopDrop.new(length, cols)
|
||||||
|
context['tablerowloop'.freeze] = tablerowloop
|
||||||
|
|
||||||
|
collection.each do |item|
|
||||||
context[@variable_name] = item
|
context[@variable_name] = item
|
||||||
context['tablerowloop'.freeze] = {
|
|
||||||
'length'.freeze => length,
|
|
||||||
'index'.freeze => index + 1,
|
|
||||||
'index0'.freeze => index,
|
|
||||||
'col'.freeze => col + 1,
|
|
||||||
'col0'.freeze => col,
|
|
||||||
'rindex'.freeze => length - index,
|
|
||||||
'rindex0'.freeze => length - index - 1,
|
|
||||||
'first'.freeze => (index == 0),
|
|
||||||
'last'.freeze => (index == length - 1),
|
|
||||||
'col_first'.freeze => (col == 0),
|
|
||||||
'col_last'.freeze => (col == cols - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
col += 1
|
output << "<td class=\"col#{tablerowloop.col}\">"
|
||||||
|
super
|
||||||
|
output << '</td>'
|
||||||
|
|
||||||
result << "<td class=\"col#{col}\">" << super << '</td>'
|
if tablerowloop.col_last && !tablerowloop.last
|
||||||
|
output << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">"
|
||||||
if col == cols && (index != length - 1)
|
|
||||||
col = 0
|
|
||||||
row += 1
|
|
||||||
result << "</tr>\n<tr class=\"row#{row}\">"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -6,23 +6,23 @@ 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)
|
unless first_block.evaluate(context)
|
||||||
return first_block.attachment.render(context)
|
return first_block.attachment.render_to_output_buffer(context, output)
|
||||||
end
|
end
|
||||||
|
|
||||||
# After the first condition unless works just like if
|
# After the first condition unless works just like if
|
||||||
@blocks[1..-1].each do |block|
|
@blocks[1..-1].each do |block|
|
||||||
if block.evaluate(context)
|
if block.evaluate(context)
|
||||||
return block.attachment.render(context)
|
return block.attachment.render_to_output_buffer(context, output)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
''.freeze
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -19,8 +19,10 @@ module Liquid
|
|||||||
@@file_system = BlankFileSystem.new
|
@@file_system = BlankFileSystem.new
|
||||||
|
|
||||||
class TagRegistry
|
class TagRegistry
|
||||||
|
include Enumerable
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
@tags = {}
|
@tags = {}
|
||||||
@cache = {}
|
@cache = {}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -41,10 +43,14 @@ module Liquid
|
|||||||
@cache.delete(tag_name)
|
@cache.delete(tag_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def each(&block)
|
||||||
|
@tags.each(&block)
|
||||||
|
end
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
@@ -63,6 +69,11 @@ module Liquid
|
|||||||
# :error raises an error when tainted output is used
|
# :error raises an error when tainted output is used
|
||||||
attr_writer :taint_mode
|
attr_writer :taint_mode
|
||||||
|
|
||||||
|
attr_accessor :default_exception_renderer
|
||||||
|
Template.default_exception_renderer = lambda do |exception|
|
||||||
|
exception
|
||||||
|
end
|
||||||
|
|
||||||
def file_system
|
def file_system
|
||||||
@@file_system
|
@@file_system
|
||||||
end
|
end
|
||||||
@@ -80,11 +91,11 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
def error_mode
|
def error_mode
|
||||||
@error_mode || :lax
|
@error_mode ||= :lax
|
||||||
end
|
end
|
||||||
|
|
||||||
def taint_mode
|
def taint_mode
|
||||||
@taint_mode || :lax
|
@taint_mode ||= :lax
|
||||||
end
|
end
|
||||||
|
|
||||||
# Pass a module with filter methods which should be available
|
# Pass a module with filter methods which should be available
|
||||||
@@ -107,6 +118,7 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
|
@rethrow_errors = false
|
||||||
@resource_limits = ResourceLimits.new(self.class.default_resource_limits)
|
@resource_limits = ResourceLimits.new(self.class.default_resource_limits)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -160,7 +172,7 @@ module Liquid
|
|||||||
c = args.shift
|
c = args.shift
|
||||||
|
|
||||||
if @rethrow_errors
|
if @rethrow_errors
|
||||||
c.exception_handler = ->(e) { raise }
|
c.exception_renderer = ->(e) { raise }
|
||||||
end
|
end
|
||||||
|
|
||||||
c
|
c
|
||||||
@@ -175,24 +187,17 @@ module Liquid
|
|||||||
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]
|
||||||
|
|
||||||
if options[:registers].is_a?(Hash)
|
registers.merge!(options[:registers]) if options[:registers].is_a?(Hash)
|
||||||
registers.merge!(options[:registers])
|
|
||||||
end
|
|
||||||
|
|
||||||
if options[:filters]
|
apply_options_to_context(context, options)
|
||||||
context.add_filters(options[:filters])
|
when Module, Array
|
||||||
end
|
|
||||||
|
|
||||||
if options[:exception_handler]
|
|
||||||
context.exception_handler = options[:exception_handler]
|
|
||||||
end
|
|
||||||
when Module
|
|
||||||
context.add_filters(args.pop)
|
|
||||||
when Array
|
|
||||||
context.add_filters(args.pop)
|
context.add_filters(args.pop)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -202,10 +207,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
|
||||||
@@ -218,6 +222,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)
|
||||||
@@ -240,5 +248,13 @@ module Liquid
|
|||||||
yield
|
yield
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def apply_options_to_context(context, options)
|
||||||
|
context.add_filters(options[:filters]) if options[:filters]
|
||||||
|
context.global_filter = options[:global_filter] if options[:global_filter]
|
||||||
|
context.exception_renderer = options[:exception_renderer] if options[:exception_renderer]
|
||||||
|
context.strict_variables = options[:strict_variables] if options[:strict_variables]
|
||||||
|
context.strict_filters = options[:strict_filters] if options[:strict_filters]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,25 +1,31 @@
|
|||||||
module Liquid
|
module Liquid
|
||||||
class Tokenizer
|
class Tokenizer
|
||||||
attr_reader :line_number
|
attr_reader :line_number, :for_liquid_tag
|
||||||
|
|
||||||
def initialize(source, line_numbers = false)
|
def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false)
|
||||||
@source = source
|
@source = source
|
||||||
@line_number = 1 if line_numbers
|
@line_number = line_number || (line_numbers ? 1 : nil)
|
||||||
|
@for_liquid_tag = for_liquid_tag
|
||||||
@tokens = tokenize
|
@tokens = tokenize
|
||||||
end
|
end
|
||||||
|
|
||||||
def shift
|
def shift
|
||||||
token = @tokens.shift
|
token = @tokens.shift or return
|
||||||
@line_number += token.count("\n") if @line_number && token
|
|
||||||
|
if @line_number
|
||||||
|
@line_number += @for_liquid_tag ? 1 : token.count("\n")
|
||||||
|
end
|
||||||
|
|
||||||
token
|
token
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def tokenize
|
def tokenize
|
||||||
@source = @source.source if @source.respond_to?(:source)
|
|
||||||
return [] if @source.to_s.empty?
|
return [] if @source.to_s.empty?
|
||||||
|
|
||||||
|
return @source.split("\n") if @for_liquid_tag
|
||||||
|
|
||||||
tokens = @source.split(TemplateParser)
|
tokens = @source.split(TemplateParser)
|
||||||
|
|
||||||
# removes the rogue empty element at the beginning of the array
|
# removes the rogue empty element at the beginning of the array
|
||||||
|
|||||||
23
lib/liquid/usage.rb
Normal file
23
lib/liquid/usage.rb
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
module Liquid
|
||||||
|
# Usage is used to store
|
||||||
|
class Usage
|
||||||
|
@messages = {}
|
||||||
|
class << self
|
||||||
|
def enable
|
||||||
|
Liquid::Context.send(:alias_method, :try_variable_find_in_environments, :try_variable_find_in_environments_usage)
|
||||||
|
end
|
||||||
|
|
||||||
|
def disable
|
||||||
|
Liquid::Context.send(:alias_method, :try_variable_find_in_environments, :try_variable_find_in_environments_original)
|
||||||
|
end
|
||||||
|
|
||||||
|
def track(message)
|
||||||
|
@messages[message] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
def results
|
||||||
|
@messages
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
26
lib/liquid/usages/try_variables.rb
Normal file
26
lib/liquid/usages/try_variables.rb
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
module Liquid
|
||||||
|
class Context
|
||||||
|
alias try_variable_find_in_environments_original try_variable_find_in_environments
|
||||||
|
|
||||||
|
def try_variable_find_in_environments_usage(key, raise_on_not_found:)
|
||||||
|
Usage.track("Using try_variable_find_in_environment")
|
||||||
|
@environments.each do |environment|
|
||||||
|
found_variable = lookup_and_evaluate(environment, key, raise_on_not_found: raise_on_not_found)
|
||||||
|
if !found_variable.nil? || @strict_variables && raise_on_not_found
|
||||||
|
return found_variable
|
||||||
|
end
|
||||||
|
|
||||||
|
Usage.track("try_variable_find_in_environment reports Nil but responds to key") if environment.key?(key)
|
||||||
|
end
|
||||||
|
@static_environments.each do |environment|
|
||||||
|
found_variable = lookup_and_evaluate(environment, key, raise_on_not_found: raise_on_not_found)
|
||||||
|
if !found_variable.nil? || @strict_variables && raise_on_not_found
|
||||||
|
return found_variable
|
||||||
|
end
|
||||||
|
|
||||||
|
Usage.track("try_variable_find_in_environment reports Nil but responds to key") if environment.key?(key)
|
||||||
|
end
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
1
lib/liquid/usages/usage_enabled.rb
Normal file
1
lib/liquid/usages/usage_enabled.rb
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Liquid::Usage.track("Usage is enabled")
|
||||||
@@ -46,13 +46,17 @@ module Liquid
|
|||||||
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
|
||||||
0
|
if obj.respond_to?(:to_number)
|
||||||
|
obj.to_number
|
||||||
|
else
|
||||||
|
0
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -72,7 +76,7 @@ module Liquid
|
|||||||
when String
|
when String
|
||||||
Time.parse(obj)
|
Time.parse(obj)
|
||||||
end
|
end
|
||||||
rescue ArgumentError
|
rescue ::ArgumentError
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -10,10 +10,16 @@ module Liquid
|
|||||||
# {{ user | link }}
|
# {{ user | link }}
|
||||||
#
|
#
|
||||||
class Variable
|
class Variable
|
||||||
|
FilterMarkupRegex = /#{FilterSeparator}\s*(.*)/om
|
||||||
FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
|
FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
|
||||||
|
FilterArgsRegex = /(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o
|
||||||
|
JustTagAttributes = /\A#{TagAttributes}\z/o
|
||||||
|
MarkupWithQuotedFragment = /(#{QuotedFragment})(.*)/om
|
||||||
|
|
||||||
attr_accessor :filters, :name, :line_number
|
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 +41,17 @@ module Liquid
|
|||||||
|
|
||||||
def lax_parse(markup)
|
def lax_parse(markup)
|
||||||
@filters = []
|
@filters = []
|
||||||
return unless markup =~ /(#{QuotedFragment})(.*)/om
|
return unless markup =~ MarkupWithQuotedFragment
|
||||||
|
|
||||||
name_markup = $1
|
name_markup = $1
|
||||||
filter_markup = $2
|
filter_markup = $2
|
||||||
@name = Expression.parse(name_markup)
|
@name = Expression.parse(name_markup)
|
||||||
if filter_markup =~ /#{FilterSeparator}\s*(.*)/om
|
if filter_markup =~ FilterMarkupRegex
|
||||||
filters = $1.scan(FilterParser)
|
filters = $1.scan(FilterParser)
|
||||||
filters.each do |f|
|
filters.each do |f|
|
||||||
next unless f =~ /\w+/
|
next unless f =~ /\w+/
|
||||||
filtername = Regexp.last_match(0)
|
filtername = Regexp.last_match(0)
|
||||||
filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o).flatten
|
filterargs = f.scan(FilterArgsRegex).flatten
|
||||||
@filters << parse_filter_expressions(filtername, filterargs)
|
@filters << parse_filter_expressions(filtername, filterargs)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -73,26 +79,44 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
def render(context)
|
def render(context)
|
||||||
@filters.inject(context.evaluate(@name)) do |output, (filter_name, filter_args, filter_kwargs)|
|
obj = @filters.inject(context.evaluate(@name)) do |output, (filter_name, filter_args, filter_kwargs)|
|
||||||
filter_args = evaluate_filter_expressions(context, filter_args, filter_kwargs)
|
filter_args = evaluate_filter_expressions(context, filter_args, filter_kwargs)
|
||||||
context.invoke(filter_name, output, *filter_args)
|
context.invoke(filter_name, output, *filter_args)
|
||||||
end.tap{ |obj| taint_check(context, obj) }
|
end
|
||||||
|
|
||||||
|
obj = context.apply_global_filter(obj)
|
||||||
|
taint_check(context, obj)
|
||||||
|
obj
|
||||||
|
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
|
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(JustTagAttributes)
|
||||||
|
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
|
||||||
|
|
||||||
@@ -126,5 +150,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
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
module Liquid
|
module Liquid
|
||||||
class VariableLookup
|
class VariableLookup
|
||||||
SQUARE_BRACKETED = /\A\[(.*)\]\z/m
|
SQUARE_BRACKETED = /\A\[(.*)\]\z/m
|
||||||
COMMAND_METHODS = ['size'.freeze, 'first'.freeze, 'last'.freeze]
|
COMMAND_METHODS = ['size'.freeze, 'first'.freeze, 'last'.freeze].freeze
|
||||||
|
|
||||||
attr_reader :name, :lookups
|
attr_reader :name, :lookups
|
||||||
|
|
||||||
@@ -55,9 +55,11 @@ module Liquid
|
|||||||
object = object.send(key).to_liquid
|
object = object.send(key).to_liquid
|
||||||
|
|
||||||
# No key was present with the desired value and it wasn't one of the directly supported
|
# No key was present with the desired value and it wasn't one of the directly supported
|
||||||
# keywords either. The only thing we got left is to return nil
|
# keywords either. The only thing we got left is to return nil or
|
||||||
|
# raise an exception if `strict_variables` option is set to true
|
||||||
else
|
else
|
||||||
return nil
|
return nil unless context.strict_variables
|
||||||
|
raise Liquid::UndefinedVariable, "undefined variable #{key}"
|
||||||
end
|
end
|
||||||
|
|
||||||
# If we are dealing with a drop here we have to
|
# If we are dealing with a drop here we have to
|
||||||
@@ -76,5 +78,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
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
# encoding: utf-8
|
# encoding: utf-8
|
||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
VERSION = "4.0.0.alpha"
|
VERSION = "4.0.3".freeze
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
# encoding: utf-8
|
# encoding: utf-8
|
||||||
|
|
||||||
lib = File.expand_path('../lib/', __FILE__)
|
lib = File.expand_path('../lib/', __FILE__)
|
||||||
$LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib)
|
$LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib)
|
||||||
|
|
||||||
@@ -15,15 +16,16 @@ Gem::Specification.new do |s|
|
|||||||
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.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(MIT-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'
|
s.add_development_dependency 'rake', '~> 11.3'
|
||||||
s.add_development_dependency 'minitest'
|
s.add_development_dependency 'minitest'
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
|
|||||||
profiler = ThemeRunner.new
|
profiler = ThemeRunner.new
|
||||||
|
|
||||||
Benchmark.ips do |x|
|
Benchmark.ips do |x|
|
||||||
x.time = 60
|
x.time = 10
|
||||||
x.warmup = 5
|
x.warmup = 5
|
||||||
|
|
||||||
puts
|
puts
|
||||||
@@ -13,5 +13,6 @@ Benchmark.ips do |x|
|
|||||||
puts
|
puts
|
||||||
|
|
||||||
x.report("parse:") { profiler.compile }
|
x.report("parse:") { profiler.compile }
|
||||||
x.report("parse & run:") { profiler.run }
|
x.report("render:") { profiler.render }
|
||||||
|
x.report("parse & render:") { profiler.run }
|
||||||
end
|
end
|
||||||
|
|||||||
63
performance/memory_profile.rb
Normal file
63
performance/memory_profile.rb
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'benchmark/ips'
|
||||||
|
require 'memory_profiler'
|
||||||
|
require 'terminal-table'
|
||||||
|
require_relative 'theme_runner'
|
||||||
|
|
||||||
|
class Profiler
|
||||||
|
LOG_LABEL = "Profiling: ".rjust(14).freeze
|
||||||
|
REPORTS_DIR = File.expand_path('.memprof', __dir__).freeze
|
||||||
|
|
||||||
|
def self.run
|
||||||
|
puts
|
||||||
|
yield new
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
@allocated = []
|
||||||
|
@retained = []
|
||||||
|
@headings = []
|
||||||
|
end
|
||||||
|
|
||||||
|
def profile(phase, &block)
|
||||||
|
print LOG_LABEL
|
||||||
|
print "#{phase}.. ".ljust(10)
|
||||||
|
report = MemoryProfiler.report(&block)
|
||||||
|
puts 'Done.'
|
||||||
|
@headings << phase.capitalize
|
||||||
|
@allocated << "#{report.scale_bytes(report.total_allocated_memsize)} (#{report.total_allocated} objects)"
|
||||||
|
@retained << "#{report.scale_bytes(report.total_retained_memsize)} (#{report.total_retained} objects)"
|
||||||
|
|
||||||
|
return if ENV['CI']
|
||||||
|
|
||||||
|
require 'fileutils'
|
||||||
|
report_file = File.join(REPORTS_DIR, "#{sanitize(phase)}.txt")
|
||||||
|
FileUtils.mkdir_p(REPORTS_DIR)
|
||||||
|
report.pretty_print(to_file: report_file, scale_bytes: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def tabulate
|
||||||
|
table = Terminal::Table.new(headings: @headings.unshift('Phase')) do |t|
|
||||||
|
t << @allocated.unshift('Total allocated')
|
||||||
|
t << @retained.unshift('Total retained')
|
||||||
|
end
|
||||||
|
|
||||||
|
puts
|
||||||
|
puts table
|
||||||
|
puts "\nDetailed report(s) saved to #{REPORTS_DIR}/" unless ENV['CI']
|
||||||
|
end
|
||||||
|
|
||||||
|
def sanitize(string)
|
||||||
|
string.downcase.gsub(/[\W]/, '-').squeeze('-')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
|
||||||
|
|
||||||
|
runner = ThemeRunner.new
|
||||||
|
Profiler.run do |x|
|
||||||
|
x.profile('parse') { runner.compile }
|
||||||
|
x.profile('render') { runner.render }
|
||||||
|
x.tabulate
|
||||||
|
end
|
||||||
@@ -12,7 +12,7 @@ class CommentForm < Liquid::Block
|
|||||||
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
|
||||||
@@ -23,7 +23,9 @@ class CommentForm < Liquid::Block
|
|||||||
'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
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class Paginate < Liquid::Block
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def render(context)
|
def render_to_output_buffer(context, output)
|
||||||
@context = context
|
@context = context
|
||||||
|
|
||||||
context.stack do
|
context.stack do
|
||||||
|
|||||||
@@ -21,53 +21,100 @@ class ThemeRunner
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Load all templates into memory, do this now so that
|
# Initialize a new liquid ThemeRunner instance
|
||||||
# we don't profile IO.
|
# Will load all templates into memory, do this now so that we don't profile IO.
|
||||||
def initialize
|
def initialize
|
||||||
@tests = Dir[__dir__ + '/tests/**/*.liquid'].collect do |test|
|
@tests = Dir[__dir__ + '/tests/**/*.liquid'].collect do |test|
|
||||||
next if File.basename(test) == 'theme.liquid'
|
next if File.basename(test) == 'theme.liquid'
|
||||||
|
|
||||||
theme_path = File.dirname(test) + '/theme.liquid'
|
theme_path = File.dirname(test) + '/theme.liquid'
|
||||||
|
{
|
||||||
[File.read(test), (File.file?(theme_path) ? File.read(theme_path) : nil), test]
|
liquid: File.read(test),
|
||||||
|
layout: (File.file?(theme_path) ? File.read(theme_path) : nil),
|
||||||
|
template_name: test
|
||||||
|
}
|
||||||
end.compact
|
end.compact
|
||||||
|
|
||||||
|
compile_all_tests
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# `compile` will test just the compilation portion of liquid without any templates
|
||||||
def compile
|
def compile
|
||||||
# Dup assigns because will make some changes to them
|
@tests.each do |test_hash|
|
||||||
|
Liquid::Template.new.parse(test_hash[:liquid])
|
||||||
@tests.each do |liquid, layout, template_name|
|
Liquid::Template.new.parse(test_hash[:layout])
|
||||||
tmpl = Liquid::Template.new
|
|
||||||
tmpl.parse(liquid)
|
|
||||||
tmpl = Liquid::Template.new
|
|
||||||
tmpl.parse(layout)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# `run` is called to benchmark rendering and compiling at the same time
|
||||||
def run
|
def run
|
||||||
# Dup assigns because will make some changes to them
|
each_test do |liquid, layout, assigns, page_template, template_name|
|
||||||
assigns = Database.tables.dup
|
|
||||||
|
|
||||||
@tests.each do |liquid, layout, template_name|
|
|
||||||
# Compute page_tempalte outside of profiler run, uninteresting to profiler
|
|
||||||
page_template = File.basename(template_name, File.extname(template_name))
|
|
||||||
compile_and_render(liquid, layout, assigns, page_template, template_name)
|
compile_and_render(liquid, layout, assigns, page_template, template_name)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# `render` is called to benchmark just the render portion of liquid
|
||||||
|
def render
|
||||||
|
@compiled_tests.each do |test|
|
||||||
|
tmpl = test[:tmpl]
|
||||||
|
assigns = test[:assigns]
|
||||||
|
layout = test[:layout]
|
||||||
|
|
||||||
|
if layout
|
||||||
|
assigns['content_for_layout'] = tmpl.render!(assigns)
|
||||||
|
layout.render!(assigns)
|
||||||
|
else
|
||||||
|
tmpl.render!(assigns)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
def compile_and_render(template, layout, assigns, page_template, template_file)
|
def compile_and_render(template, layout, assigns, page_template, template_file)
|
||||||
|
compiled_test = compile_test(template, layout, assigns, page_template, template_file)
|
||||||
|
assigns['content_for_layout'] = compiled_test[:tmpl].render!(assigns)
|
||||||
|
compiled_test[:layout].render!(assigns) if layout
|
||||||
|
end
|
||||||
|
|
||||||
|
def compile_all_tests
|
||||||
|
@compiled_tests = []
|
||||||
|
each_test do |liquid, layout, assigns, page_template, template_name|
|
||||||
|
@compiled_tests << compile_test(liquid, layout, assigns, page_template, template_name)
|
||||||
|
end
|
||||||
|
@compiled_tests
|
||||||
|
end
|
||||||
|
|
||||||
|
def compile_test(template, layout, assigns, page_template, template_file)
|
||||||
|
tmpl = init_template(page_template, template_file)
|
||||||
|
parsed_template = tmpl.parse(template).dup
|
||||||
|
|
||||||
|
if layout
|
||||||
|
parsed_layout = tmpl.parse(layout)
|
||||||
|
{ tmpl: parsed_template, assigns: assigns, layout: parsed_layout }
|
||||||
|
else
|
||||||
|
{ tmpl: parsed_template, assigns: assigns }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# utility method with similar functionality needed in `compile_all_tests` and `run`
|
||||||
|
def each_test
|
||||||
|
# Dup assigns because will make some changes to them
|
||||||
|
assigns = Database.tables.dup
|
||||||
|
|
||||||
|
@tests.each do |test_hash|
|
||||||
|
# Compute page_template outside of profiler run, uninteresting to profiler
|
||||||
|
page_template = File.basename(test_hash[:template_name], File.extname(test_hash[:template_name]))
|
||||||
|
yield(test_hash[:liquid], test_hash[:layout], assigns, page_template, test_hash[:template_name])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# set up a new Liquid::Template object for use in `compile_and_render` and `compile_test`
|
||||||
|
def init_template(page_template, template_file)
|
||||||
tmpl = Liquid::Template.new
|
tmpl = Liquid::Template.new
|
||||||
tmpl.assigns['page_title'] = 'Page title'
|
tmpl.assigns['page_title'] = 'Page title'
|
||||||
tmpl.assigns['template'] = page_template
|
tmpl.assigns['template'] = page_template
|
||||||
tmpl.registers[:file_system] = ThemeRunner::FileSystem.new(File.dirname(template_file))
|
tmpl.registers[:file_system] = ThemeRunner::FileSystem.new(File.dirname(template_file))
|
||||||
|
tmpl
|
||||||
content_for_layout = tmpl.parse(template).render!(assigns)
|
|
||||||
|
|
||||||
if layout
|
|
||||||
assigns['content_for_layout'] = content_for_layout
|
|
||||||
tmpl.parse(layout).render!(assigns)
|
|
||||||
else
|
|
||||||
content_for_layout
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
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,7 +30,9 @@ 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
|
||||||
|
|||||||
12
test/integration/block_test.rb
Normal file
12
test/integration/block_test.rb
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
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
|
||||||
@@ -13,7 +13,7 @@ class ContextDrop < Liquid::Drop
|
|||||||
@context['forloop.index']
|
@context['forloop.index']
|
||||||
end
|
end
|
||||||
|
|
||||||
def before_method(method)
|
def liquid_method_missing(method)
|
||||||
@context[method]
|
@context[method]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -30,8 +30,8 @@ class ProductDrop < Liquid::Drop
|
|||||||
end
|
end
|
||||||
|
|
||||||
class CatchallDrop < Liquid::Drop
|
class CatchallDrop < Liquid::Drop
|
||||||
def before_method(method)
|
def liquid_method_missing(method)
|
||||||
'method: ' << method.to_s
|
'catchall_method: ' << method.to_s
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ class ProductDrop < Liquid::Drop
|
|||||||
end
|
end
|
||||||
|
|
||||||
class EnumerableDrop < Liquid::Drop
|
class EnumerableDrop < Liquid::Drop
|
||||||
def before_method(method)
|
def liquid_method_missing(method)
|
||||||
method
|
method
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ end
|
|||||||
class RealEnumerableDrop < Liquid::Drop
|
class RealEnumerableDrop < Liquid::Drop
|
||||||
include Enumerable
|
include Enumerable
|
||||||
|
|
||||||
def before_method(method)
|
def liquid_method_missing(method)
|
||||||
method
|
method
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -157,14 +157,14 @@ class DropsTest < Minitest::Test
|
|||||||
assert_equal ' text1 ', output
|
assert_equal ' text1 ', output
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_unknown_method
|
def test_catchall_unknown_method
|
||||||
output = Liquid::Template.parse(' {{ product.catchall.unknown }} ').render!('product' => ProductDrop.new)
|
output = Liquid::Template.parse(' {{ product.catchall.unknown }} ').render!('product' => ProductDrop.new)
|
||||||
assert_equal ' method: unknown ', output
|
assert_equal ' catchall_method: unknown ', output
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_integer_argument_drop
|
def test_catchall_integer_argument_drop
|
||||||
output = Liquid::Template.parse(' {{ product.catchall[8] }} ').render!('product' => ProductDrop.new)
|
output = Liquid::Template.parse(' {{ product.catchall[8] }} ').render!('product' => ProductDrop.new)
|
||||||
assert_equal ' method: 8 ', output
|
assert_equal ' catchall_method: 8 ', output
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_text_array_drop
|
def test_text_array_drop
|
||||||
@@ -231,7 +231,7 @@ class DropsTest < Minitest::Test
|
|||||||
assert_equal '3', Liquid::Template.parse('{{collection.size}}').render!('collection' => EnumerableDrop.new)
|
assert_equal '3', Liquid::Template.parse('{{collection.size}}').render!('collection' => EnumerableDrop.new)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_enumerable_drop_will_invoke_before_method_for_clashing_method_names
|
def test_enumerable_drop_will_invoke_liquid_method_missing_for_clashing_method_names
|
||||||
["select", "each", "map", "cycle"].each do |method|
|
["select", "each", "map", "cycle"].each do |method|
|
||||||
assert_equal method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)
|
assert_equal method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)
|
||||||
assert_equal method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)
|
assert_equal method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)
|
||||||
|
|||||||
@@ -94,7 +94,23 @@ class ErrorHandlingTest < Minitest::Test
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
assert_match /Liquid syntax error \(line 4\)/, err.message
|
assert_match(/Liquid syntax error \(line 4\)/, err.message)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_with_line_numbers_adds_numbers_to_parser_errors_with_whitespace_trim
|
||||||
|
err = assert_raises(SyntaxError) do
|
||||||
|
Liquid::Template.parse(%q(
|
||||||
|
foobar
|
||||||
|
|
||||||
|
{%- "cat" | foobar -%}
|
||||||
|
|
||||||
|
bla
|
||||||
|
),
|
||||||
|
line_numbers: true
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_match(/Liquid syntax error \(line 4\)/, err.message)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_parsing_warn_with_line_numbers_adds_numbers_to_lexer_errors
|
def test_parsing_warn_with_line_numbers_adds_numbers_to_lexer_errors
|
||||||
@@ -107,7 +123,7 @@ class ErrorHandlingTest < Minitest::Test
|
|||||||
',
|
',
|
||||||
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)
|
||||||
@@ -124,7 +140,7 @@ class ErrorHandlingTest < Minitest::Test
|
|||||||
',
|
',
|
||||||
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
|
||||||
@@ -142,7 +158,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
|
||||||
@@ -186,22 +202,40 @@ class ErrorHandlingTest < Minitest::Test
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_exception_handler_with_string_result
|
def test_default_exception_renderer_with_internal_error
|
||||||
template = Liquid::Template.parse('This is an argument error: {{ errors.argument_error }}')
|
|
||||||
output = template.render({ 'errors' => ErrorDrop.new }, exception_handler: ->(e) { '' })
|
|
||||||
assert_equal 'This is an argument error: ', output
|
|
||||||
assert_equal [ArgumentError], template.errors.map(&:class)
|
|
||||||
end
|
|
||||||
|
|
||||||
class InternalError < Liquid::Error
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_exception_handler_with_exception_result
|
|
||||||
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)
|
||||||
handler = ->(e) { e.is_a?(Liquid::Error) ? e : InternalError.new('internal') }
|
|
||||||
output = template.render({ 'errors' => ErrorDrop.new }, exception_handler: handler)
|
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 [InternalError], template.errors.map(&:class)
|
assert_equal [Liquid::InternalError], template.errors.map(&:class)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_setting_default_exception_renderer
|
||||||
|
old_exception_renderer = Liquid::Template.default_exception_renderer
|
||||||
|
exceptions = []
|
||||||
|
Liquid::Template.default_exception_renderer = ->(e) { exceptions << e; '' }
|
||||||
|
template = Liquid::Template.parse('This is a runtime error: {{ errors.argument_error }}')
|
||||||
|
|
||||||
|
output = template.render({ 'errors' => ErrorDrop.new })
|
||||||
|
|
||||||
|
assert_equal 'This is a runtime error: ', output
|
||||||
|
assert_equal [Liquid::ArgumentError], template.errors.map(&:class)
|
||||||
|
ensure
|
||||||
|
Liquid::Template.default_exception_renderer = old_exception_renderer if old_exception_renderer
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_exception_renderer_exposing_non_liquid_error
|
||||||
|
template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true)
|
||||||
|
exceptions = []
|
||||||
|
handler = ->(e) { exceptions << e; e.cause }
|
||||||
|
|
||||||
|
output = template.render({ 'errors' => ErrorDrop.new }, exception_renderer: handler)
|
||||||
|
|
||||||
|
assert_equal 'This is a runtime error: runtime error', output
|
||||||
|
assert_equal [Liquid::InternalError], exceptions.map(&:class)
|
||||||
|
assert_equal exceptions, template.errors
|
||||||
|
assert_equal '#<RuntimeError: runtime error>', exceptions.first.cause.inspect
|
||||||
end
|
end
|
||||||
|
|
||||||
class TestFileSystem
|
class TestFileSystem
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
require 'test_helper'
|
require 'test_helper'
|
||||||
|
|
||||||
module MoneyFilter
|
|
||||||
def money(input)
|
|
||||||
sprintf(' %d$ ', input)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
module CanadianMoneyFilter
|
|
||||||
def money(input)
|
|
||||||
sprintf(' %d$ CAD ', input)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class HashOrderingTest < Minitest::Test
|
class HashOrderingTest < Minitest::Test
|
||||||
|
module MoneyFilter
|
||||||
|
def money(input)
|
||||||
|
sprintf(' %d$ ', input)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
module CanadianMoneyFilter
|
||||||
|
def money(input)
|
||||||
|
sprintf(' %d$ CAD ', input)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
include Liquid
|
include Liquid
|
||||||
|
|
||||||
def test_global_register_order
|
def test_global_register_order
|
||||||
|
|||||||
@@ -43,6 +43,14 @@ class OutputTest < Minitest::Test
|
|||||||
assert_equal expected, Template.parse(text).render!(@assigns)
|
assert_equal expected, Template.parse(text).render!(@assigns)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_variable_traversing_with_two_brackets
|
||||||
|
text = %({{ site.data.menu[include.menu][include.locale] }})
|
||||||
|
assert_equal "it works!", Template.parse(text).render!(
|
||||||
|
"site" => { "data" => { "menu" => { "foo" => { "bar" => "it works!" } } } },
|
||||||
|
"include" => { "menu" => "foo", "locale" => "bar" }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
def test_variable_traversing
|
def test_variable_traversing
|
||||||
text = %( {{car.bmw}} {{car.gm}} {{car.bmw}} )
|
text = %( {{car.bmw}} {{car.gm}} {{car.bmw}} )
|
||||||
|
|
||||||
|
|||||||
247
test/integration/parse_tree_visitor_test.rb
Normal file
247
test/integration/parse_tree_visitor_test.rb
Normal 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(
|
||||||
|
["test", "infilter"],
|
||||||
|
visit(%({{ test | split: infilter }}))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_dynamic_variable
|
||||||
|
assert_equal(
|
||||||
|
["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
|
||||||
@@ -99,7 +99,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
|
||||||
|
|
||||||
@@ -115,4 +115,8 @@ class ParsingQuirksTest < Minitest::Test
|
|||||||
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
|
||||||
|
|
||||||
|
def test_contains_in_id
|
||||||
|
assert_template_result(' YES ', '{% if containsallshipments == true %} YES {% endif %}', 'containsallshipments' => true)
|
||||||
|
end
|
||||||
end # ParsingQuirksTest
|
end # ParsingQuirksTest
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ end
|
|||||||
class SecurityTest < Minitest::Test
|
class SecurityTest < Minitest::Test
|
||||||
include Liquid
|
include Liquid
|
||||||
|
|
||||||
|
def setup
|
||||||
|
@assigns = {}
|
||||||
|
end
|
||||||
|
|
||||||
def test_no_instance_eval
|
def test_no_instance_eval
|
||||||
text = %( {{ '1+1' | instance_eval }} )
|
text = %( {{ '1+1' | instance_eval }} )
|
||||||
expected = %( 1+1 )
|
expected = %( 1+1 )
|
||||||
@@ -59,4 +63,18 @@ class SecurityTest < Minitest::Test
|
|||||||
|
|
||||||
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
|
||||||
|
depth = Liquid::Block::MAX_DEPTH
|
||||||
|
code = "{% if true %}" * depth + "rendered" + "{% endif %}" * depth
|
||||||
|
assert_equal "rendered", Template.parse(code).render!
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_more_than_max_depth_nested_blocks_raises_exception
|
||||||
|
depth = Liquid::Block::MAX_DEPTH + 1
|
||||||
|
code = "{% if true %}" * depth + "rendered" + "{% endif %}" * depth
|
||||||
|
assert_raises(Liquid::StackLevelError) do
|
||||||
|
Template.parse(code).render!
|
||||||
|
end
|
||||||
|
end
|
||||||
end # SecurityTest
|
end # SecurityTest
|
||||||
|
|||||||
@@ -41,6 +41,16 @@ class TestEnumerable < Liquid::Drop
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
class NumberLikeThing < Liquid::Drop
|
||||||
|
def initialize(amount)
|
||||||
|
@amount = amount
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_number
|
||||||
|
@amount
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
class StandardFiltersTest < Minitest::Test
|
class StandardFiltersTest < Minitest::Test
|
||||||
include Liquid
|
include Liquid
|
||||||
|
|
||||||
@@ -105,20 +115,29 @@ class StandardFiltersTest < Minitest::Test
|
|||||||
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)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_split
|
def test_split
|
||||||
assert_equal ['12', '34'], @filters.split('12~34', '~')
|
assert_equal ['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', '~')
|
||||||
# Regexp works although Liquid does not support.
|
|
||||||
assert_equal ['A', 'Z'], @filters.split('AxZ', /x/)
|
|
||||||
assert_equal [], @filters.split(nil, ' ')
|
assert_equal [], @filters.split(nil, ' ')
|
||||||
|
assert_equal ['A', 'Z'], @filters.split('A1Z', 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_escape
|
def test_escape
|
||||||
assert_equal '<strong>', @filters.escape('<strong>')
|
assert_equal '<strong>', @filters.escape('<strong>')
|
||||||
|
assert_equal '1', @filters.escape(1)
|
||||||
|
assert_equal '2001-02-03', @filters.escape(Date.new(2001, 2, 3))
|
||||||
|
assert_nil @filters.escape(nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_h
|
||||||
assert_equal '<strong>', @filters.h('<strong>')
|
assert_equal '<strong>', @filters.h('<strong>')
|
||||||
|
assert_equal '1', @filters.h(1)
|
||||||
|
assert_equal '2001-02-03', @filters.h(Date.new(2001, 2, 3))
|
||||||
|
assert_nil @filters.h(nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_escape_once
|
def test_escape_once
|
||||||
@@ -127,7 +146,22 @@ class StandardFiltersTest < Minitest::Test
|
|||||||
|
|
||||||
def test_url_encode
|
def test_url_encode
|
||||||
assert_equal 'foo%2B1%40example.com', @filters.url_encode('foo+1@example.com')
|
assert_equal 'foo%2B1%40example.com', @filters.url_encode('foo+1@example.com')
|
||||||
assert_equal nil, @filters.url_encode(nil)
|
assert_equal '1', @filters.url_encode(1)
|
||||||
|
assert_equal '2001-02-03', @filters.url_encode(Date.new(2001, 2, 3))
|
||||||
|
assert_nil @filters.url_encode(nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_url_decode
|
||||||
|
assert_equal 'foo bar', @filters.url_decode('foo+bar')
|
||||||
|
assert_equal 'foo bar', @filters.url_decode('foo%20bar')
|
||||||
|
assert_equal 'foo+1@example.com', @filters.url_decode('foo%2B1%40example.com')
|
||||||
|
assert_equal '1', @filters.url_decode(1)
|
||||||
|
assert_equal '2001-02-03', @filters.url_decode(Date.new(2001, 2, 3))
|
||||||
|
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
|
||||||
@@ -136,6 +170,7 @@ class StandardFiltersTest < Minitest::Test
|
|||||||
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” x 5.5” x 10” high) baskets fit inside one large basket (13”...', @filters.truncatewords('Two small (13” x 5.5” x 10” high) baskets fit inside one large basket (13” x 16” x 10.5” high) with cover.', 15)
|
assert_equal 'Two small (13” x 5.5” x 10” high) baskets fit inside one large basket (13”...', @filters.truncatewords('Two small (13” x 5.5” x 10” high) baskets fit inside one large basket (13” x 16” x 10.5” 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)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_strip_html
|
def test_strip_html
|
||||||
@@ -146,11 +181,15 @@ class StandardFiltersTest < Minitest::Test
|
|||||||
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
|
||||||
assert_equal '1 2 3 4', @filters.join([1, 2, 3, 4])
|
assert_equal '1 2 3 4', @filters.join([1, 2, 3, 4])
|
||||||
assert_equal '1 - 2 - 3 - 4', @filters.join([1, 2, 3, 4], ' - ')
|
assert_equal '1 - 2 - 3 - 4', @filters.join([1, 2, 3, 4], ' - ')
|
||||||
|
assert_equal '1121314', @filters.join([1, 2, 3, 4], 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_sort
|
def test_sort
|
||||||
@@ -158,6 +197,112 @@ class StandardFiltersTest < Minitest::Test
|
|||||||
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
|
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
|
||||||
|
|
||||||
|
def test_sort_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(input, "price")
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_sort_natural
|
||||||
|
assert_equal ["a", "B", "c", "D"], @filters.sort_natural(["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 ["a", "b", "c", "X", "Y", "Z"], @filters.sort_natural(["X", "Y", "Z", "a", "b", "c"])
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_sort_empty_array
|
||||||
|
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
|
||||||
|
|
||||||
|
def test_sort_natural_empty_array
|
||||||
|
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
|
||||||
|
|
||||||
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
|
||||||
@@ -177,6 +322,38 @@ class StandardFiltersTest < Minitest::Test
|
|||||||
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
|
||||||
|
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
|
||||||
|
|
||||||
|
def test_compact_empty_array
|
||||||
|
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
|
||||||
|
|
||||||
def test_reverse
|
def test_reverse
|
||||||
assert_equal [4, 3, 2, 1], @filters.reverse([1, 2, 3, 4])
|
assert_equal [4, 3, 2, 1], @filters.reverse([1, 2, 3, 4])
|
||||||
end
|
end
|
||||||
@@ -225,10 +402,46 @@ class StandardFiltersTest < Minitest::Test
|
|||||||
assert_template_result "testfoo", templ, "procs" => [p]
|
assert_template_result "testfoo", templ, "procs" => [p]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_map_over_drops_returning_procs
|
||||||
|
drops = [
|
||||||
|
{
|
||||||
|
"proc" => ->{ "foo" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"proc" => ->{ "bar" },
|
||||||
|
},
|
||||||
|
]
|
||||||
|
templ = '{{ drops | map: "proc" }}'
|
||||||
|
assert_template_result "foobar", templ, "drops" => drops
|
||||||
|
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
|
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
|
||||||
|
|
||||||
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
|
||||||
@@ -238,6 +451,10 @@ class StandardFiltersTest < Minitest::Test
|
|||||||
assert_template_result 'foobar', '{{ foo | last }}', 'foo' => [ThingWithToLiquid.new]
|
assert_template_result 'foobar', '{{ foo | last }}', 'foo' => [ThingWithToLiquid.new]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_truncate_calls_to_liquid
|
||||||
|
assert_template_result "wo...", '{{ foo | truncate: 5 }}', "foo" => TestThing.new
|
||||||
|
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")
|
||||||
@@ -255,23 +472,25 @@ class StandardFiltersTest < Minitest::Test
|
|||||||
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_equal nil, @filters.date(nil, "%B")
|
assert_nil @filters.date(nil, "%B")
|
||||||
|
|
||||||
assert_equal '', @filters.date('', "%B")
|
assert_equal '', @filters.date('', "%B")
|
||||||
|
|
||||||
assert_equal "07/05/2006", @filters.date(1152098955, "%m/%d/%Y")
|
with_timezone("UTC") do
|
||||||
assert_equal "07/05/2006", @filters.date("1152098955", "%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
|
||||||
|
|
||||||
def test_first_last
|
def test_first_last
|
||||||
assert_equal 1, @filters.first([1, 2, 3])
|
assert_equal 1, @filters.first([1, 2, 3])
|
||||||
assert_equal 3, @filters.last([1, 2, 3])
|
assert_equal 3, @filters.last([1, 2, 3])
|
||||||
assert_equal nil, @filters.first([])
|
assert_nil @filters.first([])
|
||||||
assert_equal nil, @filters.last([])
|
assert_nil @filters.last([])
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_replace
|
def test_replace
|
||||||
@@ -321,20 +540,38 @@ class StandardFiltersTest < Minitest::Test
|
|||||||
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)
|
||||||
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)
|
||||||
|
end
|
||||||
|
|
||||||
|
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 "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 }}"
|
||||||
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 "4", "{{ price | times:2 }}", 'price' => NumberLikeThing.new(2)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_divided_by
|
def test_divided_by
|
||||||
@@ -348,6 +585,8 @@ class StandardFiltersTest < Minitest::Test
|
|||||||
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)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_modulo
|
def test_modulo
|
||||||
@@ -355,6 +594,8 @@ class StandardFiltersTest < Minitest::Test
|
|||||||
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)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_round
|
def test_round
|
||||||
@@ -364,6 +605,9 @@ class StandardFiltersTest < Minitest::Test
|
|||||||
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 "4", "{{ price | round }}", 'price' => NumberLikeThing.new(4.3)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_ceil
|
def test_ceil
|
||||||
@@ -372,6 +616,8 @@ class StandardFiltersTest < Minitest::Test
|
|||||||
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)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_floor
|
def test_floor
|
||||||
@@ -380,6 +626,30 @@ class StandardFiltersTest < Minitest::Test
|
|||||||
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)
|
||||||
|
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
|
||||||
@@ -393,8 +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(TypeError) do
|
assert_raises(Liquid::ArgumentError, "concat filter requires an array argument") do
|
||||||
# no implicit conversion of Fixnum into Array
|
|
||||||
@filters.concat([1, 2], 10)
|
@filters.concat([1, 2], 10)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -417,4 +686,91 @@ class StandardFiltersTest < Minitest::Test
|
|||||||
def test_cannot_access_private_methods
|
def test_cannot_access_private_methods
|
||||||
assert_template_result('a', "{{ 'a' | to_number }}")
|
assert_template_result('a', "{{ 'a' | to_number }}")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_date_raises_nothing
|
||||||
|
assert_template_result('', "{{ '' | date: '%D' }}")
|
||||||
|
assert_template_result('abc', "{{ 'abc' | date: '%D' }}")
|
||||||
|
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
|
||||||
|
|
||||||
|
def with_timezone(tz)
|
||||||
|
old_tz = ENV['TZ']
|
||||||
|
ENV['TZ'] = tz
|
||||||
|
yield
|
||||||
|
ensure
|
||||||
|
ENV['TZ'] = old_tz
|
||||||
|
end
|
||||||
end # StandardFiltersTest
|
end # StandardFiltersTest
|
||||||
|
|||||||
11
test/integration/tags/echo_test.rb
Normal file
11
test/integration/tags/echo_test.rb
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
require 'test_helper'
|
||||||
|
|
||||||
|
class EchoTest < Minitest::Test
|
||||||
|
include Liquid
|
||||||
|
|
||||||
|
def test_echo_outputs_its_input
|
||||||
|
assert_template_result('BAR', <<~LIQUID, { 'variable-name' => 'bar' })
|
||||||
|
{%- echo variable-name | upcase -%}
|
||||||
|
LIQUID
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -103,6 +103,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
|
||||||
@@ -159,7 +187,7 @@ HERE
|
|||||||
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%}
|
||||||
@@ -178,7 +206,7 @@ HERE
|
|||||||
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
|
||||||
|
|||||||
@@ -29,10 +29,10 @@ class IfElseTagTest < Minitest::Test
|
|||||||
assert_template_result(' YES ', '{% if a or b %} YES {% endif %}', 'a' => true, 'b' => true)
|
assert_template_result(' YES ', '{% if a or b %} YES {% endif %}', 'a' => true, 'b' => true)
|
||||||
assert_template_result(' YES ', '{% if a or b %} YES {% endif %}', 'a' => true, 'b' => false)
|
assert_template_result(' YES ', '{% if a or b %} YES {% endif %}', 'a' => true, 'b' => false)
|
||||||
assert_template_result(' YES ', '{% if a or b %} YES {% endif %}', 'a' => false, 'b' => true)
|
assert_template_result(' YES ', '{% if a or b %} YES {% endif %}', 'a' => false, 'b' => true)
|
||||||
assert_template_result('', '{% if a or b %} YES {% endif %}', 'a' => false, 'b' => false)
|
assert_template_result('', '{% if a or b %} YES {% endif %}', 'a' => false, 'b' => false)
|
||||||
|
|
||||||
assert_template_result(' YES ', '{% if a or b or c %} YES {% endif %}', 'a' => false, 'b' => false, 'c' => true)
|
assert_template_result(' YES ', '{% if a or b or c %} YES {% endif %}', 'a' => false, 'b' => false, 'c' => true)
|
||||||
assert_template_result('', '{% if a or b or c %} YES {% endif %}', 'a' => false, 'b' => false, 'c' => false)
|
assert_template_result('', '{% if a or b or c %} YES {% endif %}', 'a' => false, 'b' => false, 'c' => false)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_if_or_with_operators
|
def test_if_or_with_operators
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ class TestFileSystem
|
|||||||
when 'assignments'
|
when 'assignments'
|
||||||
"{% assign foo = 'bar' %}"
|
"{% assign foo = 'bar' %}"
|
||||||
|
|
||||||
|
when 'break'
|
||||||
|
"{% break %}"
|
||||||
|
|
||||||
else
|
else
|
||||||
template_path
|
template_path
|
||||||
end
|
end
|
||||||
@@ -63,8 +66,9 @@ class CustomInclude < Liquid::Tag
|
|||||||
def parse(tokens)
|
def parse(tokens)
|
||||||
end
|
end
|
||||||
|
|
||||||
def render(context)
|
def render_to_output_buffer(context, output)
|
||||||
@template_name[1..-2]
|
output << @template_name[1..-2]
|
||||||
|
output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -137,7 +141,7 @@ class IncludeTagTest < Minitest::Test
|
|||||||
|
|
||||||
Liquid::Template.file_system = infinite_file_system.new
|
Liquid::Template.file_system = infinite_file_system.new
|
||||||
|
|
||||||
assert_raises(Liquid::StackLevelError, SystemStackError) do
|
assert_raises(Liquid::StackLevelError) do
|
||||||
Template.parse("{% include 'loop' %}").render!
|
Template.parse("{% include 'loop' %}").render!
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -217,6 +221,17 @@ class IncludeTagTest < Minitest::Test
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_render_raise_argument_error_when_template_is_undefined
|
||||||
|
assert_raises(Liquid::ArgumentError) do
|
||||||
|
template = Liquid::Template.parse('{% include undefined_variable %}')
|
||||||
|
template.render!
|
||||||
|
end
|
||||||
|
assert_raises(Liquid::ArgumentError) do
|
||||||
|
template = Liquid::Template.parse('{% include nil %}')
|
||||||
|
template.render!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def test_including_via_variable_value
|
def test_including_via_variable_value
|
||||||
assert_template_result "from TestFileSystem", "{% assign page = 'pick_a_source' %}{% include page %}"
|
assert_template_result "from TestFileSystem", "{% assign page = 'pick_a_source' %}{% include page %}"
|
||||||
|
|
||||||
@@ -224,4 +239,16 @@ class IncludeTagTest < Minitest::Test
|
|||||||
|
|
||||||
assert_template_result "Product: Draft 151cm ", "{% assign page = 'product' %}{% include page for foo %}", "foo" => { 'title' => 'Draft 151cm' }
|
assert_template_result "Product: Draft 151cm ", "{% assign page = 'product' %}{% include page for foo %}", "foo" => { 'title' => 'Draft 151cm' }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_including_with_strict_variables
|
||||||
|
template = Liquid::Template.parse("{% include 'simple' %}", error_mode: :warn)
|
||||||
|
template.render(nil, strict_variables: true)
|
||||||
|
|
||||||
|
assert_equal [], template.errors
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_break_through_include
|
||||||
|
assert_template_result "1", "{% for i in (1..3) %}{{ i }}{% break %}{{ i }}{% endfor %}"
|
||||||
|
assert_template_result "1", "{% for i in (1..3) %}{{ i }}{% include 'break' %}{{ i }}{% endfor %}"
|
||||||
|
end
|
||||||
end # IncludeTagTest
|
end # IncludeTagTest
|
||||||
|
|||||||
104
test/integration/tags/liquid_tag_test.rb
Normal file
104
test/integration/tags/liquid_tag_test.rb
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
require 'test_helper'
|
||||||
|
|
||||||
|
class LiquidTagTest < Minitest::Test
|
||||||
|
include Liquid
|
||||||
|
|
||||||
|
def test_liquid_tag
|
||||||
|
assert_template_result('1 2 3', <<~LIQUID, 'array' => [1, 2, 3])
|
||||||
|
{%- liquid
|
||||||
|
echo array | join: " "
|
||||||
|
-%}
|
||||||
|
LIQUID
|
||||||
|
|
||||||
|
assert_template_result('1 2 3', <<~LIQUID, 'array' => [1, 2, 3])
|
||||||
|
{%- liquid
|
||||||
|
for value in array
|
||||||
|
echo value
|
||||||
|
unless forloop.last
|
||||||
|
echo " "
|
||||||
|
endunless
|
||||||
|
endfor
|
||||||
|
-%}
|
||||||
|
LIQUID
|
||||||
|
|
||||||
|
assert_template_result('4 8 12 6', <<~LIQUID, 'array' => [1, 2, 3])
|
||||||
|
{%- liquid
|
||||||
|
for value in array
|
||||||
|
assign double_value = value | times: 2
|
||||||
|
echo double_value | times: 2
|
||||||
|
unless forloop.last
|
||||||
|
echo " "
|
||||||
|
endunless
|
||||||
|
endfor
|
||||||
|
|
||||||
|
echo " "
|
||||||
|
echo double_value
|
||||||
|
-%}
|
||||||
|
LIQUID
|
||||||
|
|
||||||
|
assert_template_result('abc', <<~LIQUID)
|
||||||
|
{%- liquid echo "a" -%}
|
||||||
|
b
|
||||||
|
{%- liquid echo "c" -%}
|
||||||
|
LIQUID
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_liquid_tag_errors
|
||||||
|
assert_match_syntax_error("syntax error (line 1): Unknown tag 'error'", <<~LIQUID)
|
||||||
|
{%- liquid error no such tag -%}
|
||||||
|
LIQUID
|
||||||
|
|
||||||
|
assert_match_syntax_error("syntax error (line 7): Unknown tag 'error'", <<~LIQUID)
|
||||||
|
{{ test }}
|
||||||
|
|
||||||
|
{%-
|
||||||
|
liquid
|
||||||
|
for value in array
|
||||||
|
|
||||||
|
error no such tag
|
||||||
|
endfor
|
||||||
|
-%}
|
||||||
|
LIQUID
|
||||||
|
|
||||||
|
assert_match_syntax_error("syntax error (line 2): Unknown tag '!!! the guards are vigilant'", <<~LIQUID)
|
||||||
|
{%- liquid
|
||||||
|
!!! the guards are vigilant
|
||||||
|
-%}
|
||||||
|
LIQUID
|
||||||
|
|
||||||
|
assert_match_syntax_error("syntax error (line 4): 'for' tag was never closed", <<~LIQUID)
|
||||||
|
{%- liquid
|
||||||
|
for value in array
|
||||||
|
echo 'forgot to close the for tag'
|
||||||
|
-%}
|
||||||
|
LIQUID
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_line_number_is_correct_after_a_blank_token
|
||||||
|
assert_match_syntax_error("syntax error (line 3): Unknown tag 'error'", "{% liquid echo ''\n\n error %}")
|
||||||
|
assert_match_syntax_error("syntax error (line 3): Unknown tag 'error'", "{% liquid echo ''\n \n error %}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_cannot_open_blocks_living_past_a_liquid_tag
|
||||||
|
assert_match_syntax_error("syntax error (line 3): 'if' tag was never closed", <<~LIQUID)
|
||||||
|
{%- liquid
|
||||||
|
if true
|
||||||
|
-%}
|
||||||
|
{%- endif -%}
|
||||||
|
LIQUID
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_quirk_can_close_blocks_created_before_a_liquid_tag
|
||||||
|
assert_template_result("42", <<~LIQUID)
|
||||||
|
{%- if true -%}
|
||||||
|
42
|
||||||
|
{%- liquid endif -%}
|
||||||
|
LIQUID
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_liquid_tag_in_raw
|
||||||
|
assert_template_result("{% liquid echo 'test' %}\n", <<~LIQUID)
|
||||||
|
{% raw %}{% liquid echo 'test' %}{% endraw %}
|
||||||
|
LIQUID
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -24,8 +24,8 @@ class RawTagTest < Minitest::Test
|
|||||||
end
|
end
|
||||||
|
|
||||||
def test_invalid_raw
|
def test_invalid_raw
|
||||||
assert_match_syntax_error /tag was never closed/, '{% raw %} foo'
|
assert_match_syntax_error(/tag was never closed/, '{% raw %} foo')
|
||||||
assert_match_syntax_error /Valid syntax/, '{% raw } foo {% endraw %}'
|
assert_match_syntax_error(/Valid syntax/, '{% raw } foo {% endraw %}')
|
||||||
assert_match_syntax_error /Valid syntax/, '{% raw } foo %}{% endraw %}'
|
assert_match_syntax_error(/Valid syntax/, '{% raw } foo %}{% endraw %}')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
149
test/integration/tags/render_tag_test.rb
Normal file
149
test/integration/tags/render_tag_test.rb
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
require 'test_helper'
|
||||||
|
|
||||||
|
class RenderTagTest < Minitest::Test
|
||||||
|
include Liquid
|
||||||
|
|
||||||
|
def test_render_with_no_arguments
|
||||||
|
Liquid::Template.file_system = StubFileSystem.new('source' => 'rendered content')
|
||||||
|
assert_template_result 'rendered content', '{% render "source" %}'
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_render_tag_looks_for_file_system_in_registers_first
|
||||||
|
file_system = StubFileSystem.new('pick_a_source' => 'from register file system')
|
||||||
|
assert_equal 'from register file system',
|
||||||
|
Template.parse('{% render "pick_a_source" %}').render!({}, registers: { file_system: file_system })
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_render_passes_named_arguments_into_inner_scope
|
||||||
|
Liquid::Template.file_system = StubFileSystem.new('product' => '{{ inner_product.title }}')
|
||||||
|
assert_template_result 'My Product', '{% render "product", inner_product: outer_product %}',
|
||||||
|
'outer_product' => { 'title' => 'My Product' }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_render_accepts_literals_as_arguments
|
||||||
|
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ price }}')
|
||||||
|
assert_template_result '123', '{% render "snippet", price: 123 %}'
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_render_accepts_multiple_named_arguments
|
||||||
|
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ one }} {{ two }}')
|
||||||
|
assert_template_result '1 2', '{% render "snippet", one: 1, two: 2 %}'
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_render_does_not_inherit_parent_scope_variables
|
||||||
|
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ outer_variable }}')
|
||||||
|
assert_template_result '', '{% assign outer_variable = "should not be visible" %}{% render "snippet" %}'
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_render_does_not_inherit_variable_with_same_name_as_snippet
|
||||||
|
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ snippet }}')
|
||||||
|
assert_template_result '', "{% assign snippet = 'should not be visible' %}{% render 'snippet' %}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_render_sets_the_correct_template_name_for_errors
|
||||||
|
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ unsafe }}')
|
||||||
|
|
||||||
|
with_taint_mode :error do
|
||||||
|
template = Liquid::Template.parse('{% render "snippet", unsafe: unsafe %}')
|
||||||
|
context = Context.new('unsafe' => (+'unsafe').tap(&:taint))
|
||||||
|
template.render(context)
|
||||||
|
|
||||||
|
assert_equal [Liquid::TaintedError], template.errors.map(&:class)
|
||||||
|
assert_equal 'snippet', template.errors.first.template_name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_render_sets_the_correct_template_name_for_warnings
|
||||||
|
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ unsafe }}')
|
||||||
|
|
||||||
|
with_taint_mode :warn do
|
||||||
|
template = Liquid::Template.parse('{% render "snippet", unsafe: unsafe %}')
|
||||||
|
context = Context.new('unsafe' => (+'unsafe').tap(&:taint))
|
||||||
|
template.render(context)
|
||||||
|
|
||||||
|
assert_equal [Liquid::TaintedError], context.warnings.map(&:class)
|
||||||
|
assert_equal 'snippet', context.warnings.first.template_name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_render_does_not_mutate_parent_scope
|
||||||
|
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{% assign inner = 1 %}')
|
||||||
|
assert_template_result '', "{% render 'snippet' %}{{ inner }}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_nested_render_tag
|
||||||
|
Liquid::Template.file_system = StubFileSystem.new(
|
||||||
|
'one' => "one {% render 'two' %}",
|
||||||
|
'two' => 'two'
|
||||||
|
)
|
||||||
|
assert_template_result 'one two', "{% render 'one' %}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_recursively_rendered_template_does_not_produce_endless_loop
|
||||||
|
Liquid::Template.file_system = StubFileSystem.new('loop' => '{% render "loop" %}')
|
||||||
|
|
||||||
|
assert_raises Liquid::StackLevelError do
|
||||||
|
Template.parse('{% render "loop" %}').render!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_includes_and_renders_count_towards_the_same_recursion_limit
|
||||||
|
Liquid::Template.file_system = StubFileSystem.new(
|
||||||
|
'loop_render' => '{% render "loop_include" %}',
|
||||||
|
'loop_include' => '{% include "loop_render" %}'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_raises Liquid::StackLevelError do
|
||||||
|
Template.parse('{% render "loop_include" %}').render!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_dynamically_choosen_templates_are_not_allowed
|
||||||
|
Liquid::Template.file_system = StubFileSystem.new('snippet' => 'should not be rendered')
|
||||||
|
|
||||||
|
assert_raises Liquid::SyntaxError do
|
||||||
|
Liquid::Template.parse("{% assign name = 'snippet' %}{% render name %}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_include_tag_caches_second_read_of_same_partial
|
||||||
|
file_system = StubFileSystem.new('snippet' => 'echo')
|
||||||
|
assert_equal 'echoecho',
|
||||||
|
Template.parse('{% render "snippet" %}{% render "snippet" %}')
|
||||||
|
.render!({}, registers: { file_system: file_system })
|
||||||
|
assert_equal 1, file_system.file_read_count
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_render_tag_doesnt_cache_partials_across_renders
|
||||||
|
file_system = StubFileSystem.new('snippet' => 'my message')
|
||||||
|
|
||||||
|
assert_equal 'my message',
|
||||||
|
Template.parse('{% include "snippet" %}').render!({}, registers: { file_system: file_system })
|
||||||
|
assert_equal 1, file_system.file_read_count
|
||||||
|
|
||||||
|
assert_equal 'my message',
|
||||||
|
Template.parse('{% include "snippet" %}').render!({}, registers: { file_system: file_system })
|
||||||
|
assert_equal 2, file_system.file_read_count
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_render_tag_within_if_statement
|
||||||
|
Liquid::Template.file_system = StubFileSystem.new('snippet' => 'my message')
|
||||||
|
assert_template_result 'my message', '{% if true %}{% render "snippet" %}{% endif %}'
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_break_through_render
|
||||||
|
Liquid::Template.file_system = StubFileSystem.new('break' => '{% break %}')
|
||||||
|
assert_template_result '1', '{% for i in (1..3) %}{{ i }}{% break %}{{ i }}{% endfor %}'
|
||||||
|
assert_template_result '112233', '{% for i in (1..3) %}{{ i }}{% render "break" %}{{ i }}{% endfor %}'
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_increment_is_isolated_between_renders
|
||||||
|
Liquid::Template.file_system = StubFileSystem.new('incr' => '{% increment %}')
|
||||||
|
assert_template_result '010', '{% increment %}{% increment %}{% render "incr" %}'
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_decrement_is_isolated_between_renders
|
||||||
|
Liquid::Template.file_system = StubFileSystem.new('decr' => '{% decrement %}')
|
||||||
|
assert_template_result '-1-2-1', '{% decrement %}{% decrement %}{% render "decr" %}'
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -2,7 +2,7 @@ require 'test_helper'
|
|||||||
require 'timeout'
|
require 'timeout'
|
||||||
|
|
||||||
class TemplateContextDrop < Liquid::Drop
|
class TemplateContextDrop < Liquid::Drop
|
||||||
def before_method(method)
|
def liquid_method_missing(method)
|
||||||
method
|
method
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -27,6 +27,12 @@ class ErroneousDrop < Liquid::Drop
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
class DropWithUndefinedMethod < Liquid::Drop
|
||||||
|
def foo
|
||||||
|
'foo'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
class TemplateTest < Minitest::Test
|
class TemplateTest < Minitest::Test
|
||||||
include Liquid
|
include Liquid
|
||||||
|
|
||||||
@@ -133,6 +139,27 @@ class TemplateTest < Minitest::Test
|
|||||||
refute_nil t.resource_limits.assign_score
|
refute_nil t.resource_limits.assign_score
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_resource_limits_assign_score_counts_bytes_not_characters
|
||||||
|
t = Template.parse("{% assign foo = 'すごい' %}")
|
||||||
|
t.render
|
||||||
|
assert_equal 9, t.resource_limits.assign_score
|
||||||
|
|
||||||
|
t = Template.parse("{% capture foo %}すごい{% endcapture %}")
|
||||||
|
t.render
|
||||||
|
assert_equal 9, t.resource_limits.assign_score
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_resource_limits_assign_score_nested
|
||||||
|
t = Template.parse("{% assign foo = 'aaaa' | reverse %}")
|
||||||
|
|
||||||
|
t.resource_limits.assign_score_limit = 3
|
||||||
|
assert_equal "Liquid error: Memory limits exceeded", t.render
|
||||||
|
assert t.resource_limits.reached?
|
||||||
|
|
||||||
|
t.resource_limits.assign_score_limit = 5
|
||||||
|
assert_equal "", t.render!
|
||||||
|
end
|
||||||
|
|
||||||
def test_resource_limits_aborts_rendering_after_first_error
|
def test_resource_limits_aborts_rendering_after_first_error
|
||||||
t = Template.parse("{% for a in (1..100) %} foo1 {% endfor %} bar {% for a in (1..100) %} foo2 {% endfor %}")
|
t = Template.parse("{% for a in (1..100) %} foo1 {% endfor %} bar {% for a in (1..100) %} foo2 {% endfor %}")
|
||||||
t.resource_limits.render_score_limit = 50
|
t.resource_limits.render_score_limit = 50
|
||||||
@@ -170,6 +197,14 @@ class TemplateTest < Minitest::Test
|
|||||||
assert_equal "ababab", t.render
|
assert_equal "ababab", t.render
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_render_length_uses_number_of_bytes_not_characters
|
||||||
|
t = Template.parse("{% if true %}すごい{% endif %}")
|
||||||
|
t.resource_limits.render_length_limit = 10
|
||||||
|
assert_equal "Liquid error: Memory limits exceeded", t.render
|
||||||
|
t.resource_limits.render_length_limit = 18
|
||||||
|
assert_equal "すごい", t.render
|
||||||
|
end
|
||||||
|
|
||||||
def test_default_resource_limits_unaffected_by_render_with_context
|
def test_default_resource_limits_unaffected_by_render_with_context
|
||||||
context = Context.new
|
context = Context.new
|
||||||
t = Template.parse("{% for a in (1..100) %} {% assign foo = 1 %} {% endfor %}")
|
t = Template.parse("{% for a in (1..100) %} {% assign foo = 1 %} {% endfor %}")
|
||||||
@@ -198,17 +233,118 @@ class TemplateTest < Minitest::Test
|
|||||||
assert_equal 'ruby error in drop', e.message
|
assert_equal 'ruby error in drop', e.message
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_exception_handler_doesnt_reraise_if_it_returns_false
|
def test_exception_renderer_that_returns_string
|
||||||
exception = nil
|
exception = nil
|
||||||
Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_handler: ->(e) { exception = e; false })
|
handler = ->(e) { exception = e; '<!-- error -->' }
|
||||||
|
|
||||||
|
output = Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_renderer: handler)
|
||||||
|
|
||||||
assert exception.is_a?(Liquid::ZeroDivisionError)
|
assert exception.is_a?(Liquid::ZeroDivisionError)
|
||||||
|
assert_equal '<!-- error -->', output
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_exception_handler_does_reraise_if_it_returns_true
|
def test_exception_renderer_that_raises
|
||||||
exception = nil
|
exception = nil
|
||||||
assert_raises(Liquid::ZeroDivisionError) do
|
assert_raises(Liquid::ZeroDivisionError) do
|
||||||
Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_handler: ->(e) { exception = e; true })
|
Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_renderer: ->(e) { exception = e; raise })
|
||||||
end
|
end
|
||||||
assert exception.is_a?(Liquid::ZeroDivisionError)
|
assert exception.is_a?(Liquid::ZeroDivisionError)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_global_filter_option_on_render
|
||||||
|
global_filter_proc = ->(output) { "#{output} filtered" }
|
||||||
|
rendered_template = Template.parse("{{name}}").render({ "name" => "bob" }, global_filter: global_filter_proc)
|
||||||
|
|
||||||
|
assert_equal 'bob filtered', rendered_template
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_global_filter_option_when_native_filters_exist
|
||||||
|
global_filter_proc = ->(output) { "#{output} filtered" }
|
||||||
|
rendered_template = Template.parse("{{name | upcase}}").render({ "name" => "bob" }, global_filter: global_filter_proc)
|
||||||
|
|
||||||
|
assert_equal 'BOB filtered', rendered_template
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_undefined_variables
|
||||||
|
t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}")
|
||||||
|
result = t.render({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, { strict_variables: true })
|
||||||
|
|
||||||
|
assert_equal '33 32 ', result
|
||||||
|
assert_equal 3, t.errors.count
|
||||||
|
assert_instance_of Liquid::UndefinedVariable, t.errors[0]
|
||||||
|
assert_equal 'Liquid error: undefined variable y', t.errors[0].message
|
||||||
|
assert_instance_of Liquid::UndefinedVariable, t.errors[1]
|
||||||
|
assert_equal 'Liquid error: undefined variable b', t.errors[1].message
|
||||||
|
assert_instance_of Liquid::UndefinedVariable, t.errors[2]
|
||||||
|
assert_equal 'Liquid error: undefined variable d', t.errors[2].message
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_nil_value_does_not_raise
|
||||||
|
Liquid::Template.error_mode = :strict
|
||||||
|
t = Template.parse("some{{x}}thing")
|
||||||
|
result = t.render!({ 'x' => nil }, strict_variables: true)
|
||||||
|
|
||||||
|
assert_equal 0, t.errors.count
|
||||||
|
assert_equal 'something', result
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_undefined_variables_raise
|
||||||
|
t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}")
|
||||||
|
|
||||||
|
assert_raises UndefinedVariable do
|
||||||
|
t.render!({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, { strict_variables: true })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_undefined_drop_methods
|
||||||
|
d = DropWithUndefinedMethod.new
|
||||||
|
t = Template.new.parse('{{ foo }} {{ woot }}')
|
||||||
|
result = t.render(d, { strict_variables: true })
|
||||||
|
|
||||||
|
assert_equal 'foo ', result
|
||||||
|
assert_equal 1, t.errors.count
|
||||||
|
assert_instance_of Liquid::UndefinedDropMethod, t.errors[0]
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_undefined_drop_methods_raise
|
||||||
|
d = DropWithUndefinedMethod.new
|
||||||
|
t = Template.new.parse('{{ foo }} {{ woot }}')
|
||||||
|
|
||||||
|
assert_raises UndefinedDropMethod do
|
||||||
|
t.render!(d, { strict_variables: true })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_undefined_filters
|
||||||
|
t = Template.parse("{{a}} {{x | upcase | somefilter1 | somefilter2 | somefilter3}}")
|
||||||
|
filters = Module.new do
|
||||||
|
def somefilter3(v)
|
||||||
|
"-#{v}-"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
result = t.render({ 'a' => 123, 'x' => 'foo' }, { filters: [filters], strict_filters: true })
|
||||||
|
|
||||||
|
assert_equal '123 ', result
|
||||||
|
assert_equal 1, t.errors.count
|
||||||
|
assert_instance_of Liquid::UndefinedFilter, t.errors[0]
|
||||||
|
assert_equal 'Liquid error: undefined filter somefilter1', t.errors[0].message
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_undefined_filters_raise
|
||||||
|
t = Template.parse("{{x | somefilter1 | upcase | somefilter2}}")
|
||||||
|
|
||||||
|
assert_raises UndefinedFilter do
|
||||||
|
t.render!({ 'x' => 'foo' }, { strict_filters: true })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_using_range_literal_works_as_expected
|
||||||
|
t = Template.parse("{% assign foo = (x..y) %}{{ foo }}")
|
||||||
|
result = t.render({ 'x' => 1, 'y' => 5 })
|
||||||
|
assert_equal '1..5', result
|
||||||
|
|
||||||
|
t = Template.parse("{% assign nums = (x..y) %}{% for num in nums %}{{ num }}{% endfor %}")
|
||||||
|
result = t.render({ 'x' => 1, 'y' => 5 })
|
||||||
|
assert_equal '12345', result
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
529
test/integration/trim_mode_test.rb
Normal file
529
test/integration/trim_mode_test.rb
Normal file
@@ -0,0 +1,529 @@
|
|||||||
|
require 'test_helper'
|
||||||
|
|
||||||
|
class TrimModeTest < Minitest::Test
|
||||||
|
include Liquid
|
||||||
|
|
||||||
|
# Make sure the trim isn't applied to standard output
|
||||||
|
def test_standard_output
|
||||||
|
text = <<-END_TEMPLATE
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
{{ 'John' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
END_TEMPLATE
|
||||||
|
expected = <<-END_EXPECTED
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
John
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
END_EXPECTED
|
||||||
|
assert_template_result(expected, text)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_variable_output_with_multiple_blank_lines
|
||||||
|
text = <<-END_TEMPLATE
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
|
||||||
|
|
||||||
|
{{- 'John' -}}
|
||||||
|
|
||||||
|
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
END_TEMPLATE
|
||||||
|
expected = <<-END_EXPECTED
|
||||||
|
<div>
|
||||||
|
<p>John</p>
|
||||||
|
</div>
|
||||||
|
END_EXPECTED
|
||||||
|
assert_template_result(expected, text)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_tag_output_with_multiple_blank_lines
|
||||||
|
text = <<-END_TEMPLATE
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
|
||||||
|
|
||||||
|
{%- if true -%}
|
||||||
|
yes
|
||||||
|
{%- endif -%}
|
||||||
|
|
||||||
|
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
END_TEMPLATE
|
||||||
|
expected = <<-END_EXPECTED
|
||||||
|
<div>
|
||||||
|
<p>yes</p>
|
||||||
|
</div>
|
||||||
|
END_EXPECTED
|
||||||
|
assert_template_result(expected, text)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Make sure the trim isn't applied to standard tags
|
||||||
|
def test_standard_tags
|
||||||
|
whitespace = ' '
|
||||||
|
text = <<-END_TEMPLATE
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
{% if true %}
|
||||||
|
yes
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
END_TEMPLATE
|
||||||
|
expected = <<-END_EXPECTED
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
#{whitespace}
|
||||||
|
yes
|
||||||
|
#{whitespace}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
END_EXPECTED
|
||||||
|
assert_template_result(expected, text)
|
||||||
|
|
||||||
|
text = <<-END_TEMPLATE
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
{% if false %}
|
||||||
|
no
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
END_TEMPLATE
|
||||||
|
expected = <<-END_EXPECTED
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
#{whitespace}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
END_EXPECTED
|
||||||
|
assert_template_result(expected, text)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Make sure the trim isn't too agressive
|
||||||
|
def test_no_trim_output
|
||||||
|
text = '<p>{{- \'John\' -}}</p>'
|
||||||
|
expected = '<p>John</p>'
|
||||||
|
assert_template_result(expected, text)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Make sure the trim isn't too agressive
|
||||||
|
def test_no_trim_tags
|
||||||
|
text = '<p>{%- if true -%}yes{%- endif -%}</p>'
|
||||||
|
expected = '<p>yes</p>'
|
||||||
|
assert_template_result(expected, text)
|
||||||
|
|
||||||
|
text = '<p>{%- if false -%}no{%- endif -%}</p>'
|
||||||
|
expected = '<p></p>'
|
||||||
|
assert_template_result(expected, text)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_single_line_outer_tag
|
||||||
|
text = '<p> {%- if true %} yes {% endif -%} </p>'
|
||||||
|
expected = '<p> yes </p>'
|
||||||
|
assert_template_result(expected, text)
|
||||||
|
|
||||||
|
text = '<p> {%- if false %} no {% endif -%} </p>'
|
||||||
|
expected = '<p></p>'
|
||||||
|
assert_template_result(expected, text)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_single_line_inner_tag
|
||||||
|
text = '<p> {% if true -%} yes {%- endif %} </p>'
|
||||||
|
expected = '<p> yes </p>'
|
||||||
|
assert_template_result(expected, text)
|
||||||
|
|
||||||
|
text = '<p> {% if false -%} no {%- endif %} </p>'
|
||||||
|
expected = '<p> </p>'
|
||||||
|
assert_template_result(expected, text)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_single_line_post_tag
|
||||||
|
text = '<p> {% if true -%} yes {% endif -%} </p>'
|
||||||
|
expected = '<p> yes </p>'
|
||||||
|
assert_template_result(expected, text)
|
||||||
|
|
||||||
|
text = '<p> {% if false -%} no {% endif -%} </p>'
|
||||||
|
expected = '<p> </p>'
|
||||||
|
assert_template_result(expected, text)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_single_line_pre_tag
|
||||||
|
text = '<p> {%- if true %} yes {%- endif %} </p>'
|
||||||
|
expected = '<p> yes </p>'
|
||||||
|
assert_template_result(expected, text)
|
||||||
|
|
||||||
|
text = '<p> {%- if false %} no {%- endif %} </p>'
|
||||||
|
expected = '<p> </p>'
|
||||||
|
assert_template_result(expected, text)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_pre_trim_output
|
||||||
|
text = <<-END_TEMPLATE
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
{{- 'John' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
END_TEMPLATE
|
||||||
|
expected = <<-END_EXPECTED
|
||||||
|
<div>
|
||||||
|
<p>John
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
END_EXPECTED
|
||||||
|
assert_template_result(expected, text)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_pre_trim_tags
|
||||||
|
text = <<-END_TEMPLATE
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
{%- if true %}
|
||||||
|
yes
|
||||||
|
{%- endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
END_TEMPLATE
|
||||||
|
expected = <<-END_EXPECTED
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
yes
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
END_EXPECTED
|
||||||
|
assert_template_result(expected, text)
|
||||||
|
|
||||||
|
text = <<-END_TEMPLATE
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
{%- if false %}
|
||||||
|
no
|
||||||
|
{%- endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
END_TEMPLATE
|
||||||
|
expected = <<-END_EXPECTED
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
END_EXPECTED
|
||||||
|
assert_template_result(expected, text)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_post_trim_output
|
||||||
|
text = <<-END_TEMPLATE
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
{{ 'John' -}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
END_TEMPLATE
|
||||||
|
expected = <<-END_EXPECTED
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
John</p>
|
||||||
|
</div>
|
||||||
|
END_EXPECTED
|
||||||
|
assert_template_result(expected, text)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_post_trim_tags
|
||||||
|
text = <<-END_TEMPLATE
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
{% if true -%}
|
||||||
|
yes
|
||||||
|
{% endif -%}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
END_TEMPLATE
|
||||||
|
expected = <<-END_EXPECTED
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
yes
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
END_EXPECTED
|
||||||
|
assert_template_result(expected, text)
|
||||||
|
|
||||||
|
text = <<-END_TEMPLATE
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
{% if false -%}
|
||||||
|
no
|
||||||
|
{% endif -%}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
END_TEMPLATE
|
||||||
|
expected = <<-END_EXPECTED
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
END_EXPECTED
|
||||||
|
assert_template_result(expected, text)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_pre_and_post_trim_tags
|
||||||
|
text = <<-END_TEMPLATE
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
{%- if true %}
|
||||||
|
yes
|
||||||
|
{% endif -%}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
END_TEMPLATE
|
||||||
|
expected = <<-END_EXPECTED
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
yes
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
END_EXPECTED
|
||||||
|
assert_template_result(expected, text)
|
||||||
|
|
||||||
|
text = <<-END_TEMPLATE
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
{%- if false %}
|
||||||
|
no
|
||||||
|
{% endif -%}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
END_TEMPLATE
|
||||||
|
expected = <<-END_EXPECTED
|
||||||
|
<div>
|
||||||
|
<p></p>
|
||||||
|
</div>
|
||||||
|
END_EXPECTED
|
||||||
|
assert_template_result(expected, text)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_post_and_pre_trim_tags
|
||||||
|
text = <<-END_TEMPLATE
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
{% if true -%}
|
||||||
|
yes
|
||||||
|
{%- endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
END_TEMPLATE
|
||||||
|
expected = <<-END_EXPECTED
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
yes
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
END_EXPECTED
|
||||||
|
assert_template_result(expected, text)
|
||||||
|
|
||||||
|
whitespace = ' '
|
||||||
|
text = <<-END_TEMPLATE
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
{% if false -%}
|
||||||
|
no
|
||||||
|
{%- endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
END_TEMPLATE
|
||||||
|
expected = <<-END_EXPECTED
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
#{whitespace}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
END_EXPECTED
|
||||||
|
assert_template_result(expected, text)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_trim_output
|
||||||
|
text = <<-END_TEMPLATE
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
{{- 'John' -}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
END_TEMPLATE
|
||||||
|
expected = <<-END_EXPECTED
|
||||||
|
<div>
|
||||||
|
<p>John</p>
|
||||||
|
</div>
|
||||||
|
END_EXPECTED
|
||||||
|
assert_template_result(expected, text)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_trim_tags
|
||||||
|
text = <<-END_TEMPLATE
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
{%- if true -%}
|
||||||
|
yes
|
||||||
|
{%- endif -%}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
END_TEMPLATE
|
||||||
|
expected = <<-END_EXPECTED
|
||||||
|
<div>
|
||||||
|
<p>yes</p>
|
||||||
|
</div>
|
||||||
|
END_EXPECTED
|
||||||
|
assert_template_result(expected, text)
|
||||||
|
|
||||||
|
text = <<-END_TEMPLATE
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
{%- if false -%}
|
||||||
|
no
|
||||||
|
{%- endif -%}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
END_TEMPLATE
|
||||||
|
expected = <<-END_EXPECTED
|
||||||
|
<div>
|
||||||
|
<p></p>
|
||||||
|
</div>
|
||||||
|
END_EXPECTED
|
||||||
|
assert_template_result(expected, text)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_whitespace_trim_output
|
||||||
|
text = <<-END_TEMPLATE
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
{{- 'John' -}},
|
||||||
|
{{- '30' -}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
END_TEMPLATE
|
||||||
|
expected = <<-END_EXPECTED
|
||||||
|
<div>
|
||||||
|
<p>John,30</p>
|
||||||
|
</div>
|
||||||
|
END_EXPECTED
|
||||||
|
assert_template_result(expected, text)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_whitespace_trim_tags
|
||||||
|
text = <<-END_TEMPLATE
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
{%- if true -%}
|
||||||
|
yes
|
||||||
|
{%- endif -%}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
END_TEMPLATE
|
||||||
|
expected = <<-END_EXPECTED
|
||||||
|
<div>
|
||||||
|
<p>yes</p>
|
||||||
|
</div>
|
||||||
|
END_EXPECTED
|
||||||
|
assert_template_result(expected, text)
|
||||||
|
|
||||||
|
text = <<-END_TEMPLATE
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
{%- if false -%}
|
||||||
|
no
|
||||||
|
{%- endif -%}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
END_TEMPLATE
|
||||||
|
expected = <<-END_EXPECTED
|
||||||
|
<div>
|
||||||
|
<p></p>
|
||||||
|
</div>
|
||||||
|
END_EXPECTED
|
||||||
|
assert_template_result(expected, text)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_complex_trim_output
|
||||||
|
text = <<-END_TEMPLATE
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
{{- 'John' -}}
|
||||||
|
{{- '30' -}}
|
||||||
|
</p>
|
||||||
|
<b>
|
||||||
|
{{ 'John' -}}
|
||||||
|
{{- '30' }}
|
||||||
|
</b>
|
||||||
|
<i>
|
||||||
|
{{- 'John' }}
|
||||||
|
{{ '30' -}}
|
||||||
|
</i>
|
||||||
|
</div>
|
||||||
|
END_TEMPLATE
|
||||||
|
expected = <<-END_EXPECTED
|
||||||
|
<div>
|
||||||
|
<p>John30</p>
|
||||||
|
<b>
|
||||||
|
John30
|
||||||
|
</b>
|
||||||
|
<i>John
|
||||||
|
30</i>
|
||||||
|
</div>
|
||||||
|
END_EXPECTED
|
||||||
|
assert_template_result(expected, text)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_complex_trim
|
||||||
|
text = <<-END_TEMPLATE
|
||||||
|
<div>
|
||||||
|
{%- if true -%}
|
||||||
|
{%- if true -%}
|
||||||
|
<p>
|
||||||
|
{{- 'John' -}}
|
||||||
|
</p>
|
||||||
|
{%- endif -%}
|
||||||
|
{%- endif -%}
|
||||||
|
</div>
|
||||||
|
END_TEMPLATE
|
||||||
|
expected = <<-END_EXPECTED
|
||||||
|
<div><p>John</p></div>
|
||||||
|
END_EXPECTED
|
||||||
|
assert_template_result(expected, text)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_right_trim_followed_by_tag
|
||||||
|
assert_template_result('ab c', '{{ "a" -}}{{ "b" }} c')
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_raw_output
|
||||||
|
whitespace = ' '
|
||||||
|
text = <<-END_TEMPLATE
|
||||||
|
<div>
|
||||||
|
{% raw %}
|
||||||
|
{%- if true -%}
|
||||||
|
<p>
|
||||||
|
{{- 'John' -}}
|
||||||
|
</p>
|
||||||
|
{%- endif -%}
|
||||||
|
{% endraw %}
|
||||||
|
</div>
|
||||||
|
END_TEMPLATE
|
||||||
|
expected = <<-END_EXPECTED
|
||||||
|
<div>
|
||||||
|
#{whitespace}
|
||||||
|
{%- if true -%}
|
||||||
|
<p>
|
||||||
|
{{- 'John' -}}
|
||||||
|
</p>
|
||||||
|
{%- endif -%}
|
||||||
|
#{whitespace}
|
||||||
|
</div>
|
||||||
|
END_EXPECTED
|
||||||
|
assert_template_result(expected, text)
|
||||||
|
end
|
||||||
|
end # TrimModeTest
|
||||||
14
test/integration/usages/try_variables_test.rb
Normal file
14
test/integration/usages/try_variables_test.rb
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
require 'test_helper'
|
||||||
|
|
||||||
|
class TryVariablesUsageTest < Minitest::Test
|
||||||
|
include Liquid
|
||||||
|
|
||||||
|
def test_test_usages
|
||||||
|
Usage.enable
|
||||||
|
template = Template.parse(%({{test}}))
|
||||||
|
assert_equal 'worked', template.render!('test' => 'worked')
|
||||||
|
assert_equal 'worked wonderfully', template.render!('test' => 'worked wonderfully')
|
||||||
|
assert_equal true, Usage.results["Using try_variable_find_in_environment"]
|
||||||
|
Usage.disable
|
||||||
|
end
|
||||||
|
end
|
||||||
9
test/integration/usages/usage_enabled_test.rb
Normal file
9
test/integration/usages/usage_enabled_test.rb
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
require 'test_helper'
|
||||||
|
|
||||||
|
class UsageEnabledUsageTest < Minitest::Test
|
||||||
|
include Liquid
|
||||||
|
|
||||||
|
def test_live_usages
|
||||||
|
assert_equal true, Usage.results["Usage is enabled"]
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -89,4 +89,8 @@ class VariableTest < Minitest::Test
|
|||||||
def test_multiline_variable
|
def test_multiline_variable
|
||||||
assert_equal 'worked', Template.parse("{{\ntest\n}}").render!('test' => 'worked')
|
assert_equal 'worked', Template.parse("{{\ntest\n}}").render!('test' => 'worked')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_render_symbol
|
||||||
|
assert_template_result 'bar', '{{ foo }}', 'foo' => :bar
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
30
test/test_helper.rb
Normal file → Executable file
30
test/test_helper.rb
Normal file → Executable file
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
ENV["MT_NO_EXPECTATIONS"] = "1"
|
ENV["MT_NO_EXPECTATIONS"] = "1"
|
||||||
require 'minitest/autorun'
|
require 'minitest/autorun'
|
||||||
require 'spy/integration'
|
|
||||||
|
|
||||||
$LOAD_PATH.unshift(File.join(File.expand_path(__dir__), '..', 'lib'))
|
$LOAD_PATH.unshift(File.join(File.expand_path(__dir__), '..', 'lib'))
|
||||||
require 'liquid.rb'
|
require 'liquid.rb'
|
||||||
@@ -15,7 +14,7 @@ if env_mode = ENV['LIQUID_PARSER_MODE']
|
|||||||
end
|
end
|
||||||
Liquid::Template.error_mode = mode
|
Liquid::Template.error_mode = mode
|
||||||
|
|
||||||
if ENV['LIQUID-C'] == '1'
|
if ENV['LIQUID_C'] == '1'
|
||||||
puts "-- LIQUID C"
|
puts "-- LIQUID C"
|
||||||
require 'liquid/c'
|
require 'liquid/c'
|
||||||
end
|
end
|
||||||
@@ -38,18 +37,18 @@ module Minitest
|
|||||||
include Liquid
|
include Liquid
|
||||||
|
|
||||||
def assert_template_result(expected, template, assigns = {}, message = nil)
|
def assert_template_result(expected, template, assigns = {}, message = nil)
|
||||||
assert_equal expected, Template.parse(template).render!(assigns), message
|
assert_equal expected, Template.parse(template, line_numbers: true).render!(assigns), message
|
||||||
end
|
end
|
||||||
|
|
||||||
def assert_template_result_matches(expected, template, assigns = {}, message = nil)
|
def assert_template_result_matches(expected, template, assigns = {}, message = nil)
|
||||||
return assert_template_result(expected, template, assigns, message) unless expected.is_a? Regexp
|
return assert_template_result(expected, template, assigns, message) unless expected.is_a? Regexp
|
||||||
|
|
||||||
assert_match expected, Template.parse(template).render!(assigns), message
|
assert_match expected, Template.parse(template, line_numbers: true).render!(assigns), message
|
||||||
end
|
end
|
||||||
|
|
||||||
def assert_match_syntax_error(match, template, assigns = {})
|
def assert_match_syntax_error(match, template, assigns = {})
|
||||||
exception = assert_raises(Liquid::SyntaxError) do
|
exception = assert_raises(Liquid::SyntaxError) do
|
||||||
Template.parse(template).render(assigns)
|
Template.parse(template, line_numbers: true).render(assigns)
|
||||||
end
|
end
|
||||||
assert_match match, exception.message
|
assert_match match, exception.message
|
||||||
end
|
end
|
||||||
@@ -85,6 +84,13 @@ module Minitest
|
|||||||
ensure
|
ensure
|
||||||
Liquid::Template.error_mode = old_mode
|
Liquid::Template.error_mode = old_mode
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def with_custom_tag(tag_name, tag_class)
|
||||||
|
Liquid::Template.register_tag(tag_name, tag_class)
|
||||||
|
yield
|
||||||
|
ensure
|
||||||
|
Liquid::Template.tags.delete(tag_name)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -115,3 +121,17 @@ class ErrorDrop < Liquid::Drop
|
|||||||
raise Exception, 'exception'
|
raise Exception, 'exception'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
class StubFileSystem
|
||||||
|
attr_reader :file_read_count
|
||||||
|
|
||||||
|
def initialize(values)
|
||||||
|
@file_read_count = 0
|
||||||
|
@values = values
|
||||||
|
end
|
||||||
|
|
||||||
|
def read_template_file(template_path)
|
||||||
|
@file_read_count += 1
|
||||||
|
@values.fetch(template_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|||||||
@@ -44,8 +44,47 @@ class BlockUnitTest < Minitest::Test
|
|||||||
end
|
end
|
||||||
|
|
||||||
def test_with_custom_tag
|
def test_with_custom_tag
|
||||||
Liquid::Template.register_tag("testtag", Block)
|
with_custom_tag('testtag', Block) do
|
||||||
assert Liquid::Template.parse("{% testtag %} {% endtesttag %}")
|
assert Liquid::Template.parse("{% testtag %} {% endtesttag %}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_custom_block_tags_have_a_default_render_to_output_buffer_method_for_backwards_compatibility
|
||||||
|
klass1 = Class.new(Block) do
|
||||||
|
def render(*)
|
||||||
|
'hello'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
with_custom_tag('blabla', klass1) do
|
||||||
|
template = Liquid::Template.parse("{% blabla %} bla {% endblabla %}")
|
||||||
|
|
||||||
|
assert_equal 'hello', template.render
|
||||||
|
|
||||||
|
buf = ''
|
||||||
|
output = template.render({}, output: buf)
|
||||||
|
assert_equal 'hello', output
|
||||||
|
assert_equal 'hello', buf
|
||||||
|
assert_equal buf.object_id, output.object_id
|
||||||
|
end
|
||||||
|
|
||||||
|
klass2 = Class.new(klass1) do
|
||||||
|
def render(*)
|
||||||
|
'foo' + super + 'bar'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
with_custom_tag('blabla', klass2) do
|
||||||
|
template = Liquid::Template.parse("{% blabla %} foo {% endblabla %}")
|
||||||
|
|
||||||
|
assert_equal 'foohellobar', template.render
|
||||||
|
|
||||||
|
buf = ''
|
||||||
|
output = template.render({}, output: buf)
|
||||||
|
assert_equal 'foohellobar', output
|
||||||
|
assert_equal 'foohellobar', buf
|
||||||
|
assert_equal buf.object_id, output.object_id
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -3,50 +3,54 @@ require 'test_helper'
|
|||||||
class ConditionUnitTest < Minitest::Test
|
class ConditionUnitTest < Minitest::Test
|
||||||
include Liquid
|
include Liquid
|
||||||
|
|
||||||
|
def setup
|
||||||
|
@context = Liquid::Context.new
|
||||||
|
end
|
||||||
|
|
||||||
def test_basic_condition
|
def test_basic_condition
|
||||||
assert_equal false, Condition.new(1, '==', 2).evaluate
|
assert_equal false, Condition.new(1, '==', 2).evaluate
|
||||||
assert_equal true, Condition.new(1, '==', 1).evaluate
|
assert_equal true, Condition.new(1, '==', 1).evaluate
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_default_operators_evalute_true
|
def test_default_operators_evalute_true
|
||||||
assert_evalutes_true 1, '==', 1
|
assert_evaluates_true 1, '==', 1
|
||||||
assert_evalutes_true 1, '!=', 2
|
assert_evaluates_true 1, '!=', 2
|
||||||
assert_evalutes_true 1, '<>', 2
|
assert_evaluates_true 1, '<>', 2
|
||||||
assert_evalutes_true 1, '<', 2
|
assert_evaluates_true 1, '<', 2
|
||||||
assert_evalutes_true 2, '>', 1
|
assert_evaluates_true 2, '>', 1
|
||||||
assert_evalutes_true 1, '>=', 1
|
assert_evaluates_true 1, '>=', 1
|
||||||
assert_evalutes_true 2, '>=', 1
|
assert_evaluates_true 2, '>=', 1
|
||||||
assert_evalutes_true 1, '<=', 2
|
assert_evaluates_true 1, '<=', 2
|
||||||
assert_evalutes_true 1, '<=', 1
|
assert_evaluates_true 1, '<=', 1
|
||||||
# negative numbers
|
# negative numbers
|
||||||
assert_evalutes_true 1, '>', -1
|
assert_evaluates_true 1, '>', -1
|
||||||
assert_evalutes_true -1, '<', 1
|
assert_evaluates_true -1, '<', 1
|
||||||
assert_evalutes_true 1.0, '>', -1.0
|
assert_evaluates_true 1.0, '>', -1.0
|
||||||
assert_evalutes_true -1.0, '<', 1.0
|
assert_evaluates_true -1.0, '<', 1.0
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_default_operators_evalute_false
|
def test_default_operators_evalute_false
|
||||||
assert_evalutes_false 1, '==', 2
|
assert_evaluates_false 1, '==', 2
|
||||||
assert_evalutes_false 1, '!=', 1
|
assert_evaluates_false 1, '!=', 1
|
||||||
assert_evalutes_false 1, '<>', 1
|
assert_evaluates_false 1, '<>', 1
|
||||||
assert_evalutes_false 1, '<', 0
|
assert_evaluates_false 1, '<', 0
|
||||||
assert_evalutes_false 2, '>', 4
|
assert_evaluates_false 2, '>', 4
|
||||||
assert_evalutes_false 1, '>=', 3
|
assert_evaluates_false 1, '>=', 3
|
||||||
assert_evalutes_false 2, '>=', 4
|
assert_evaluates_false 2, '>=', 4
|
||||||
assert_evalutes_false 1, '<=', 0
|
assert_evaluates_false 1, '<=', 0
|
||||||
assert_evalutes_false 1, '<=', 0
|
assert_evaluates_false 1, '<=', 0
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_contains_works_on_strings
|
def test_contains_works_on_strings
|
||||||
assert_evalutes_true 'bob', 'contains', 'o'
|
assert_evaluates_true 'bob', 'contains', 'o'
|
||||||
assert_evalutes_true 'bob', 'contains', 'b'
|
assert_evaluates_true 'bob', 'contains', 'b'
|
||||||
assert_evalutes_true 'bob', 'contains', 'bo'
|
assert_evaluates_true 'bob', 'contains', 'bo'
|
||||||
assert_evalutes_true 'bob', 'contains', 'ob'
|
assert_evaluates_true 'bob', 'contains', 'ob'
|
||||||
assert_evalutes_true 'bob', 'contains', 'bob'
|
assert_evaluates_true 'bob', 'contains', 'bob'
|
||||||
|
|
||||||
assert_evalutes_false 'bob', 'contains', 'bob2'
|
assert_evaluates_false 'bob', 'contains', 'bob2'
|
||||||
assert_evalutes_false 'bob', 'contains', 'a'
|
assert_evaluates_false 'bob', 'contains', 'a'
|
||||||
assert_evalutes_false 'bob', 'contains', '---'
|
assert_evaluates_false 'bob', 'contains', '---'
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_invalid_comparation_operator
|
def test_invalid_comparation_operator
|
||||||
@@ -60,34 +64,42 @@ class ConditionUnitTest < Minitest::Test
|
|||||||
assert_evaluates_argument_error '1', '<=', 0
|
assert_evaluates_argument_error '1', '<=', 0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_hash_compare_backwards_compatibility
|
||||||
|
assert_nil Condition.new({}, '>', 2).evaluate
|
||||||
|
assert_nil Condition.new(2, '>', {}).evaluate
|
||||||
|
assert_equal false, Condition.new({}, '==', 2).evaluate
|
||||||
|
assert_equal true, Condition.new({ 'a' => 1 }, '==', { 'a' => 1 }).evaluate
|
||||||
|
assert_equal true, Condition.new({ 'a' => 2 }, 'contains', 'a').evaluate
|
||||||
|
end
|
||||||
|
|
||||||
def test_contains_works_on_arrays
|
def test_contains_works_on_arrays
|
||||||
@context = Liquid::Context.new
|
@context = Liquid::Context.new
|
||||||
@context['array'] = [1, 2, 3, 4, 5]
|
@context['array'] = [1, 2, 3, 4, 5]
|
||||||
array_expr = VariableLookup.new("array")
|
array_expr = VariableLookup.new("array")
|
||||||
|
|
||||||
assert_evalutes_false array_expr, 'contains', 0
|
assert_evaluates_false array_expr, 'contains', 0
|
||||||
assert_evalutes_true array_expr, 'contains', 1
|
assert_evaluates_true array_expr, 'contains', 1
|
||||||
assert_evalutes_true array_expr, 'contains', 2
|
assert_evaluates_true array_expr, 'contains', 2
|
||||||
assert_evalutes_true array_expr, 'contains', 3
|
assert_evaluates_true array_expr, 'contains', 3
|
||||||
assert_evalutes_true array_expr, 'contains', 4
|
assert_evaluates_true array_expr, 'contains', 4
|
||||||
assert_evalutes_true array_expr, 'contains', 5
|
assert_evaluates_true array_expr, 'contains', 5
|
||||||
assert_evalutes_false array_expr, 'contains', 6
|
assert_evaluates_false array_expr, 'contains', 6
|
||||||
assert_evalutes_false array_expr, 'contains', "1"
|
assert_evaluates_false array_expr, 'contains', "1"
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_contains_returns_false_for_nil_operands
|
def test_contains_returns_false_for_nil_operands
|
||||||
@context = Liquid::Context.new
|
@context = Liquid::Context.new
|
||||||
assert_evalutes_false VariableLookup.new('not_assigned'), 'contains', '0'
|
assert_evaluates_false VariableLookup.new('not_assigned'), 'contains', '0'
|
||||||
assert_evalutes_false 0, 'contains', VariableLookup.new('not_assigned')
|
assert_evaluates_false 0, 'contains', VariableLookup.new('not_assigned')
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_contains_return_false_on_wrong_data_type
|
def test_contains_return_false_on_wrong_data_type
|
||||||
assert_evalutes_false 1, 'contains', 0
|
assert_evaluates_false 1, 'contains', 0
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_contains_with_string_left_operand_coerces_right_operand_to_string
|
def test_contains_with_string_left_operand_coerces_right_operand_to_string
|
||||||
assert_evalutes_true ' 1 ', 'contains', 1
|
assert_evaluates_true ' 1 ', 'contains', 1
|
||||||
assert_evalutes_false ' 1 ', 'contains', 2
|
assert_evaluates_false ' 1 ', 'contains', 2
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_or_condition
|
def test_or_condition
|
||||||
@@ -121,8 +133,8 @@ class ConditionUnitTest < Minitest::Test
|
|||||||
def test_should_allow_custom_proc_operator
|
def test_should_allow_custom_proc_operator
|
||||||
Condition.operators['starts_with'] = proc { |cond, left, right| left =~ %r{^#{right}} }
|
Condition.operators['starts_with'] = proc { |cond, left, right| left =~ %r{^#{right}} }
|
||||||
|
|
||||||
assert_evalutes_true 'bob', 'starts_with', 'b'
|
assert_evaluates_true 'bob', 'starts_with', 'b'
|
||||||
assert_evalutes_false 'bob', 'starts_with', 'o'
|
assert_evaluates_false 'bob', 'starts_with', 'o'
|
||||||
ensure
|
ensure
|
||||||
Condition.operators.delete 'starts_with'
|
Condition.operators.delete 'starts_with'
|
||||||
end
|
end
|
||||||
@@ -131,24 +143,24 @@ class ConditionUnitTest < Minitest::Test
|
|||||||
@context = Liquid::Context.new
|
@context = Liquid::Context.new
|
||||||
@context['one'] = @context['another'] = "gnomeslab-and-or-liquid"
|
@context['one'] = @context['another'] = "gnomeslab-and-or-liquid"
|
||||||
|
|
||||||
assert_evalutes_true VariableLookup.new("one"), '==', VariableLookup.new("another")
|
assert_evaluates_true VariableLookup.new("one"), '==', VariableLookup.new("another")
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def assert_evalutes_true(left, op, right)
|
def assert_evaluates_true(left, op, right)
|
||||||
assert Condition.new(left, op, right).evaluate(@context || Liquid::Context.new),
|
assert Condition.new(left, op, right).evaluate(@context),
|
||||||
"Evaluated false: #{left} #{op} #{right}"
|
"Evaluated false: #{left} #{op} #{right}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def assert_evalutes_false(left, op, right)
|
def assert_evaluates_false(left, op, right)
|
||||||
assert !Condition.new(left, op, right).evaluate(@context || Liquid::Context.new),
|
assert !Condition.new(left, op, right).evaluate(@context),
|
||||||
"Evaluated true: #{left} #{op} #{right}"
|
"Evaluated true: #{left} #{op} #{right}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def assert_evaluates_argument_error(left, op, right)
|
def assert_evaluates_argument_error(left, op, right)
|
||||||
assert_raises(Liquid::ArgumentError) do
|
assert_raises(Liquid::ArgumentError) do
|
||||||
Condition.new(left, op, right).evaluate(@context || Liquid::Context.new)
|
Condition.new(left, op, right).evaluate(@context)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end # ConditionTest
|
end # ConditionTest
|
||||||
|
|||||||
@@ -70,10 +70,6 @@ class ContextUnitTest < Minitest::Test
|
|||||||
@context = Liquid::Context.new
|
@context = Liquid::Context.new
|
||||||
end
|
end
|
||||||
|
|
||||||
def teardown
|
|
||||||
Spy.teardown
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_variables
|
def test_variables
|
||||||
@context['string'] = 'string'
|
@context['string'] = 'string'
|
||||||
assert_equal 'string', @context['string']
|
assert_equal 'string', @context['string']
|
||||||
@@ -98,12 +94,12 @@ class ContextUnitTest < Minitest::Test
|
|||||||
assert_equal false, @context['bool']
|
assert_equal false, @context['bool']
|
||||||
|
|
||||||
@context['nil'] = nil
|
@context['nil'] = nil
|
||||||
assert_equal nil, @context['nil']
|
assert_nil @context['nil']
|
||||||
assert_equal nil, @context['nil']
|
assert_nil @context['nil']
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_variables_not_existing
|
def test_variables_not_existing
|
||||||
assert_equal nil, @context['does_not_exist']
|
assert_nil @context['does_not_exist']
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_scoping
|
def test_scoping
|
||||||
@@ -185,7 +181,7 @@ class ContextUnitTest < Minitest::Test
|
|||||||
@context['test'] = 'test'
|
@context['test'] = 'test'
|
||||||
assert_equal 'test', @context['test']
|
assert_equal 'test', @context['test']
|
||||||
@context.pop
|
@context.pop
|
||||||
assert_equal nil, @context['test']
|
assert_nil @context['test']
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_hierachical_data
|
def test_hierachical_data
|
||||||
@@ -267,7 +263,7 @@ class ContextUnitTest < Minitest::Test
|
|||||||
|
|
||||||
def test_access_hashes_with_hash_notation
|
def test_access_hashes_with_hash_notation
|
||||||
@context['products'] = { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] }
|
@context['products'] = { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] }
|
||||||
@context['product'] = { 'variants' => [ { 'title' => 'draft151cm' }, { 'title' => 'element151cm' } ] }
|
@context['product'] = { 'variants' => [ { 'title' => 'draft151cm' }, { 'title' => 'element151cm' } ] }
|
||||||
|
|
||||||
assert_equal 5, @context['products["count"]']
|
assert_equal 5, @context['products["count"]']
|
||||||
assert_equal 'deepsnow', @context['products["tags"][0]']
|
assert_equal 'deepsnow', @context['products["tags"][0]']
|
||||||
@@ -300,12 +296,12 @@ class ContextUnitTest < Minitest::Test
|
|||||||
@context['hash'] = { 'first' => 'Hello' }
|
@context['hash'] = { 'first' => 'Hello' }
|
||||||
|
|
||||||
assert_equal 1, @context['array.first']
|
assert_equal 1, @context['array.first']
|
||||||
assert_equal nil, @context['array["first"]']
|
assert_nil @context['array["first"]']
|
||||||
assert_equal 'Hello', @context['hash["first"]']
|
assert_equal 'Hello', @context['hash["first"]']
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_first_can_appear_in_middle_of_callchain
|
def test_first_can_appear_in_middle_of_callchain
|
||||||
@context['product'] = { 'variants' => [ { 'title' => 'draft151cm' }, { 'title' => 'element151cm' } ] }
|
@context['product'] = { 'variants' => [ { 'title' => 'draft151cm' }, { 'title' => 'element151cm' } ] }
|
||||||
|
|
||||||
assert_equal 'draft151cm', @context['product.variants[0].title']
|
assert_equal 'draft151cm', @context['product.variants[0].title']
|
||||||
assert_equal 'element151cm', @context['product.variants[1].title']
|
assert_equal 'element151cm', @context['product.variants[1].title']
|
||||||
@@ -450,14 +446,10 @@ class ContextUnitTest < Minitest::Test
|
|||||||
assert_equal @context, @context['category'].context
|
assert_equal @context, @context['category'].context
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_use_empty_instead_of_any_in_interrupt_handling_to_avoid_lots_of_unnecessary_object_allocations
|
def test_interrupt_avoids_object_allocations
|
||||||
mock_any = Spy.on_instance_method(Array, :any?)
|
assert_no_object_allocations do
|
||||||
mock_empty = Spy.on_instance_method(Array, :empty?)
|
@context.interrupt?
|
||||||
|
end
|
||||||
@context.interrupt?
|
|
||||||
|
|
||||||
refute mock_any.has_been_called?
|
|
||||||
assert mock_empty.has_been_called?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_context_initialization_with_a_proc_in_environment
|
def test_context_initialization_with_a_proc_in_environment
|
||||||
@@ -466,4 +458,100 @@ class ContextUnitTest < Minitest::Test
|
|||||||
assert contx
|
assert contx
|
||||||
assert_nil contx['poutine']
|
assert_nil contx['poutine']
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_apply_global_filter
|
||||||
|
global_filter_proc = ->(output) { "#{output} filtered" }
|
||||||
|
|
||||||
|
context = Context.new
|
||||||
|
context.global_filter = global_filter_proc
|
||||||
|
|
||||||
|
assert_equal 'hi filtered', context.apply_global_filter('hi')
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_static_environments_are_read_with_lower_priority_than_environments
|
||||||
|
context = Context.build(
|
||||||
|
static_environments: { 'shadowed' => 'static', 'unshadowed' => 'static' },
|
||||||
|
environments: { 'shadowed' => 'dynamic' }
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_equal 'dynamic', context['shadowed']
|
||||||
|
assert_equal 'static', context['unshadowed']
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_apply_global_filter_when_no_global_filter_exist
|
||||||
|
context = Context.new
|
||||||
|
assert_equal 'hi', context.apply_global_filter('hi')
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_new_isolated_subcontext_does_not_inherit_variables
|
||||||
|
super_context = Context.new
|
||||||
|
super_context['my_variable'] = 'some value'
|
||||||
|
subcontext = super_context.new_isolated_subcontext
|
||||||
|
|
||||||
|
assert_nil subcontext['my_variable']
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_new_isolated_subcontext_inherits_static_environment
|
||||||
|
super_context = Context.build(static_environments: { 'my_environment_value' => 'my value' })
|
||||||
|
subcontext = super_context.new_isolated_subcontext
|
||||||
|
|
||||||
|
assert_equal 'my value', subcontext['my_environment_value']
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_new_isolated_subcontext_inherits_resource_limits
|
||||||
|
resource_limits = ResourceLimits.new({})
|
||||||
|
super_context = Context.new({}, {}, {}, false, resource_limits)
|
||||||
|
subcontext = super_context.new_isolated_subcontext
|
||||||
|
assert_equal resource_limits, subcontext.resource_limits
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_new_isolated_subcontext_inherits_exception_renderer
|
||||||
|
super_context = Context.new
|
||||||
|
super_context.exception_renderer = ->(_e) { 'my exception message' }
|
||||||
|
subcontext = super_context.new_isolated_subcontext
|
||||||
|
assert_equal 'my exception message', subcontext.handle_error(Liquid::Error.new)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_new_isolated_subcontext_does_not_inherit_non_static_registers
|
||||||
|
registers = {
|
||||||
|
my_register: :my_value
|
||||||
|
}
|
||||||
|
super_context = Context.new({}, {}, registers)
|
||||||
|
subcontext = super_context.new_isolated_subcontext
|
||||||
|
assert_nil subcontext.registers[:my_register]
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_new_isolated_subcontext_inherits_static_registers
|
||||||
|
super_context = Context.build(static_registers: { my_register: :my_value })
|
||||||
|
subcontext = super_context.new_isolated_subcontext
|
||||||
|
assert_equal :my_value, subcontext.static_registers[:my_register]
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_new_isolated_subcontext_inherits_filters
|
||||||
|
my_filter = Module.new do
|
||||||
|
def my_filter(*)
|
||||||
|
'my filter result'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
super_context = Context.new
|
||||||
|
super_context.add_filters([my_filter])
|
||||||
|
subcontext = super_context.new_isolated_subcontext
|
||||||
|
template = Template.parse('{{ 123 | my_filter }}')
|
||||||
|
assert_equal 'my filter result', template.render(subcontext)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def assert_no_object_allocations
|
||||||
|
unless RUBY_ENGINE == 'ruby'
|
||||||
|
skip "stackprof needed to count object allocations"
|
||||||
|
end
|
||||||
|
require 'stackprof'
|
||||||
|
|
||||||
|
profile = StackProf.run(mode: :object) do
|
||||||
|
yield
|
||||||
|
end
|
||||||
|
assert_equal 0, profile[:samples]
|
||||||
|
end
|
||||||
end # ContextTest
|
end # ContextTest
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class LexerUnitTest < Minitest::Test
|
|||||||
end
|
end
|
||||||
|
|
||||||
def test_comparison
|
def test_comparison
|
||||||
tokens = Lexer.new('== <> contains').tokenize
|
tokens = Lexer.new('== <> contains ').tokenize
|
||||||
assert_equal [[:comparison, '=='], [:comparison, '<>'], [:comparison, 'contains'], [:end_of_string]], tokens
|
assert_equal [[:comparison, '=='], [:comparison, '<>'], [:comparison, 'contains'], [:end_of_string]], tokens
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
91
test/unit/partial_cache_unit_test.rb
Normal file
91
test/unit/partial_cache_unit_test.rb
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
require 'test_helper'
|
||||||
|
|
||||||
|
class PartialCacheUnitTest < Minitest::Test
|
||||||
|
def test_uses_the_file_system_register_if_present
|
||||||
|
context = Liquid::Context.build(
|
||||||
|
registers: {
|
||||||
|
file_system: StubFileSystem.new('my_partial' => 'my partial body')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
partial = Liquid::PartialCache.load(
|
||||||
|
'my_partial',
|
||||||
|
context: context,
|
||||||
|
parse_context: Liquid::ParseContext.new
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_equal 'my partial body', partial.render
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_reads_from_the_file_system_only_once_per_file
|
||||||
|
file_system = StubFileSystem.new('my_partial' => 'some partial body')
|
||||||
|
context = Liquid::Context.build(
|
||||||
|
registers: { file_system: file_system }
|
||||||
|
)
|
||||||
|
|
||||||
|
2.times do
|
||||||
|
Liquid::PartialCache.load(
|
||||||
|
'my_partial',
|
||||||
|
context: context,
|
||||||
|
parse_context: Liquid::ParseContext.new
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_equal 1, file_system.file_read_count
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_cache_state_is_stored_per_context
|
||||||
|
parse_context = Liquid::ParseContext.new
|
||||||
|
shared_file_system = StubFileSystem.new(
|
||||||
|
'my_partial' => 'my shared value'
|
||||||
|
)
|
||||||
|
context_one = Liquid::Context.build(
|
||||||
|
registers: {
|
||||||
|
file_system: shared_file_system
|
||||||
|
}
|
||||||
|
)
|
||||||
|
context_two = Liquid::Context.build(
|
||||||
|
registers: {
|
||||||
|
file_system: shared_file_system
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
2.times do
|
||||||
|
Liquid::PartialCache.load(
|
||||||
|
'my_partial',
|
||||||
|
context: context_one,
|
||||||
|
parse_context: parse_context
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
Liquid::PartialCache.load(
|
||||||
|
'my_partial',
|
||||||
|
context: context_two,
|
||||||
|
parse_context: parse_context
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_equal 2, shared_file_system.file_read_count
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_cache_is_not_broken_when_a_different_parse_context_is_used
|
||||||
|
file_system = StubFileSystem.new('my_partial' => 'some partial body')
|
||||||
|
context = Liquid::Context.build(
|
||||||
|
registers: { file_system: file_system }
|
||||||
|
)
|
||||||
|
|
||||||
|
Liquid::PartialCache.load(
|
||||||
|
'my_partial',
|
||||||
|
context: context,
|
||||||
|
parse_context: Liquid::ParseContext.new(my_key: 'value one')
|
||||||
|
)
|
||||||
|
Liquid::PartialCache.load(
|
||||||
|
'my_partial',
|
||||||
|
context: context,
|
||||||
|
parse_context: Liquid::ParseContext.new(my_key: 'value two')
|
||||||
|
)
|
||||||
|
|
||||||
|
# Technically what we care about is that the file was parsed twice,
|
||||||
|
# but measuring file reads is an OK proxy for this.
|
||||||
|
assert_equal 1, file_system.file_read_count
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -29,6 +29,18 @@ class StrainerUnitTest < Minitest::Test
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_stainer_argument_error_contains_backtrace
|
||||||
|
strainer = Strainer.create(nil)
|
||||||
|
begin
|
||||||
|
strainer.invoke("public_filter", 1)
|
||||||
|
rescue Liquid::ArgumentError => e
|
||||||
|
assert_match(
|
||||||
|
/\ALiquid error: wrong number of arguments \((1 for 0|given 1, expected 0)\)\z/,
|
||||||
|
e.message)
|
||||||
|
assert_equal e.backtrace[0].split(':')[0], __FILE__
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def test_strainer_only_invokes_public_filter_methods
|
def test_strainer_only_invokes_public_filter_methods
|
||||||
strainer = Strainer.create(nil)
|
strainer = Strainer.create(nil)
|
||||||
assert_equal false, strainer.class.invokable?('__test__')
|
assert_equal false, strainer.class.invokable?('__test__')
|
||||||
@@ -65,4 +77,88 @@ class StrainerUnitTest < Minitest::Test
|
|||||||
assert_kind_of b, strainer
|
assert_kind_of b, strainer
|
||||||
assert_kind_of Liquid::StandardFilters, strainer
|
assert_kind_of Liquid::StandardFilters, strainer
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_add_filter_when_wrong_filter_class
|
||||||
|
c = Context.new
|
||||||
|
s = c.strainer
|
||||||
|
wrong_filter = ->(v) { v.reverse }
|
||||||
|
|
||||||
|
assert_raises ArgumentError do
|
||||||
|
s.class.add_filter(wrong_filter)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
module PrivateMethodOverrideFilter
|
||||||
|
private
|
||||||
|
|
||||||
|
def public_filter
|
||||||
|
"overriden as private"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_add_filter_raises_when_module_privately_overrides_registered_public_methods
|
||||||
|
strainer = Context.new.strainer
|
||||||
|
|
||||||
|
error = assert_raises(Liquid::MethodOverrideError) do
|
||||||
|
strainer.class.add_filter(PrivateMethodOverrideFilter)
|
||||||
|
end
|
||||||
|
assert_equal 'Liquid error: Filter overrides registered public methods as non public: public_filter', error.message
|
||||||
|
end
|
||||||
|
|
||||||
|
module ProtectedMethodOverrideFilter
|
||||||
|
protected
|
||||||
|
|
||||||
|
def public_filter
|
||||||
|
"overriden as protected"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_add_filter_raises_when_module_overrides_registered_public_method_as_protected
|
||||||
|
strainer = Context.new.strainer
|
||||||
|
|
||||||
|
error = assert_raises(Liquid::MethodOverrideError) do
|
||||||
|
strainer.class.add_filter(ProtectedMethodOverrideFilter)
|
||||||
|
end
|
||||||
|
assert_equal 'Liquid error: Filter overrides registered public methods as non public: public_filter', error.message
|
||||||
|
end
|
||||||
|
|
||||||
|
module PublicMethodOverrideFilter
|
||||||
|
def public_filter
|
||||||
|
"public"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_add_filter_does_not_raise_when_module_overrides_previously_registered_method
|
||||||
|
strainer = Context.new.strainer
|
||||||
|
strainer.class.add_filter(PublicMethodOverrideFilter)
|
||||||
|
assert strainer.class.filter_methods.include?('public_filter')
|
||||||
|
end
|
||||||
|
|
||||||
|
module LateAddedFilter
|
||||||
|
def late_added_filter(input)
|
||||||
|
"filtered"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_global_filter_clears_cache
|
||||||
|
assert_equal 'input', Strainer.create(nil).invoke('late_added_filter', 'input')
|
||||||
|
Strainer.global_filter(LateAddedFilter)
|
||||||
|
assert_equal 'filtered', Strainer.create(nil).invoke('late_added_filter', 'input')
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_add_filter_does_not_include_already_included_module
|
||||||
|
mod = Module.new do
|
||||||
|
class << self
|
||||||
|
attr_accessor :include_count
|
||||||
|
def included(mod)
|
||||||
|
self.include_count += 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self.include_count = 0
|
||||||
|
end
|
||||||
|
strainer = Context.new.strainer
|
||||||
|
strainer.class.add_filter(mod)
|
||||||
|
strainer.class.add_filter(mod)
|
||||||
|
assert_equal 1, mod.include_count
|
||||||
|
end
|
||||||
end # StrainerTest
|
end # StrainerTest
|
||||||
|
|||||||
@@ -18,4 +18,42 @@ class TagUnitTest < Minitest::Test
|
|||||||
tag = Tag.parse("some_tag", "", Tokenizer.new(""), ParseContext.new)
|
tag = Tag.parse("some_tag", "", Tokenizer.new(""), ParseContext.new)
|
||||||
assert_equal 'some_tag', tag.tag_name
|
assert_equal 'some_tag', tag.tag_name
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_custom_tags_have_a_default_render_to_output_buffer_method_for_backwards_compatibility
|
||||||
|
klass1 = Class.new(Tag) do
|
||||||
|
def render(*)
|
||||||
|
'hello'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
with_custom_tag('blabla', klass1) do
|
||||||
|
template = Liquid::Template.parse("{% blabla %}")
|
||||||
|
|
||||||
|
assert_equal 'hello', template.render
|
||||||
|
|
||||||
|
buf = ''
|
||||||
|
output = template.render({}, output: buf)
|
||||||
|
assert_equal 'hello', output
|
||||||
|
assert_equal 'hello', buf
|
||||||
|
assert_equal buf.object_id, output.object_id
|
||||||
|
end
|
||||||
|
|
||||||
|
klass2 = Class.new(klass1) do
|
||||||
|
def render(*)
|
||||||
|
'foo' + super + 'bar'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
with_custom_tag('blabla', klass2) do
|
||||||
|
template = Liquid::Template.parse("{% blabla %}")
|
||||||
|
|
||||||
|
assert_equal 'foohellobar', template.render
|
||||||
|
|
||||||
|
buf = ''
|
||||||
|
output = template.render({}, output: buf)
|
||||||
|
assert_equal 'foohellobar', output
|
||||||
|
assert_equal 'foohellobar', buf
|
||||||
|
assert_equal buf.object_id, output.object_id
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -67,4 +67,12 @@ class TemplateUnitTest < Minitest::Test
|
|||||||
Template.tags.delete('fake')
|
Template.tags.delete('fake')
|
||||||
assert_nil Template.tags['fake']
|
assert_nil Template.tags['fake']
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_tags_can_be_looped_over
|
||||||
|
Template.register_tag('fake', FakeTag)
|
||||||
|
result = Template.tags.map { |name, klass| [name, klass] }
|
||||||
|
assert result.include?(["fake", "TemplateUnitTest::FakeTag"])
|
||||||
|
ensure
|
||||||
|
Template.tags.delete('fake')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user