mirror of
https://github.com/kemko/liquid.git
synced 2026-01-01 15:55:40 +03:00
Compare commits
421 Commits
liquid-doc
...
v4.0.0.rc2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
b42d35ff36 | ||
|
|
b4e133e26f | ||
|
|
1f9bd1d809 | ||
|
|
e88be60818 | ||
|
|
14416b3c49 | ||
|
|
bde14a650d | ||
|
|
c535af021a | ||
|
|
9c9345869b | ||
|
|
73834a7e52 | ||
|
|
c45310170b | ||
|
|
920e1df643 | ||
|
|
cebf75b8d7 | ||
|
|
afda01adbb | ||
|
|
959cd6d2a2 | ||
|
|
4c1b89e20e | ||
|
|
83b6dd0268 | ||
|
|
6fb402e60d | ||
|
|
338287df5e | ||
|
|
c4c398174b | ||
|
|
80b6ac3bc7 | ||
|
|
15974d9168 | ||
|
|
f22ab4358b | ||
|
|
9cf0d264e1 | ||
|
|
575e3cae7a | ||
|
|
fad3b8275c | ||
|
|
5a071cb7f2 | ||
|
|
8cb2364179 | ||
|
|
3c23cfc167 | ||
|
|
8a8de46c6a | ||
|
|
58c7f226cc | ||
|
|
adfcd0ab13 | ||
|
|
30ef7d14b0 | ||
|
|
4920ec50e4 | ||
|
|
e395229283 | ||
|
|
9470fba0c8 | ||
|
|
ac180e8402 | ||
|
|
7c5d54aced | ||
|
|
5fbb312a67 | ||
|
|
8385099960 | ||
|
|
504b6fb3c7 | ||
|
|
01420e8014 | ||
|
|
dde35a2907 | ||
|
|
e2323332cd | ||
|
|
7b4398d0c4 | ||
|
|
1e23036b2d | ||
|
|
13716fa68b | ||
|
|
232e8bb4cd | ||
|
|
6968def5dd | ||
|
|
ad3748af21 | ||
|
|
c82e04f4e6 | ||
|
|
5919626da4 | ||
|
|
82269e2509 | ||
|
|
b347fac3c0 | ||
|
|
e761a6864e | ||
|
|
4c22cef341 | ||
|
|
c319240174 | ||
|
|
6ace095207 | ||
|
|
e36f366c33 | ||
|
|
02729e89c0 | ||
|
|
6b0f6401d0 | ||
|
|
fc8e6c8d3a | ||
|
|
79d7dd06df | ||
|
|
3a907a4db7 | ||
|
|
8b98f92c7f | ||
|
|
b79c0c611c | ||
|
|
8a2947865b | ||
|
|
ea29f8b4b8 | ||
|
|
c84f4520cc | ||
|
|
3dd6433e2f | ||
|
|
ab7109a335 | ||
|
|
94fe050952 | ||
|
|
9b98c436c4 | ||
|
|
889019f53a | ||
|
|
c290375aec | ||
|
|
719a98a25e | ||
|
|
86d8b552da | ||
|
|
b1ee9129e7 | ||
|
|
be2e41e4d5 | ||
|
|
20ca2b9632 | ||
|
|
6c058823ad | ||
|
|
27245c9eab | ||
|
|
a639a13380 | ||
|
|
05a0fe56c8 | ||
|
|
c1eb694057 | ||
|
|
f53b31c867 | ||
|
|
363388e92f | ||
|
|
873eddbb85 | ||
|
|
e790b60f60 | ||
|
|
3264d60425 | ||
|
|
8ff1b8e01f | ||
|
|
8d5e71f856 | ||
|
|
89c6e605f8 | ||
|
|
6265c36ec9 | ||
|
|
8af99ff918 | ||
|
|
36200ff704 | ||
|
|
a9c7df931f | ||
|
|
070639daba | ||
|
|
dad98cfc89 | ||
|
|
1d3c0b3dab | ||
|
|
648a4888af | ||
|
|
b4e5017c79 | ||
|
|
f1bc9f27df | ||
|
|
f4724f0db3 | ||
|
|
df74955ac4 | ||
|
|
3372ca8136 | ||
|
|
8cf524e91c | ||
|
|
5e38626309 | ||
|
|
b31df0fb3d | ||
|
|
9e815ec594 | ||
|
|
93b29b67ef | ||
|
|
863e8968f0 | ||
|
|
4c9d2009f9 | ||
|
|
239cfa5a44 | ||
|
|
8a8996387b | ||
|
|
9310640bdd | ||
|
|
4c3381a523 | ||
|
|
261aa2e726 | ||
|
|
247c51ac70 | ||
|
|
37dbec3610 | ||
|
|
ff253a04c6 | ||
|
|
25ef0df671 | ||
|
|
32460c255b | ||
|
|
724d625f47 | ||
|
|
f658dcee8b | ||
|
|
fa6cd6287e | ||
|
|
76c24db039 | ||
|
|
068791d698 | ||
|
|
3a082ddbbd | ||
|
|
03b3446119 | ||
|
|
251ce7483c | ||
|
|
4592afcc8b | ||
|
|
448766b0c4 | ||
|
|
6390652c3f | ||
|
|
f266aee2e5 | ||
|
|
df0649a031 | ||
|
|
78a5972487 | ||
|
|
298ae3357c | ||
|
|
f1f3f57647 | ||
|
|
e5dd63e1fc | ||
|
|
881f86d698 | ||
|
|
a1b209d212 | ||
|
|
8e5926669b | ||
|
|
8736b602ea | ||
|
|
b8365af07d | ||
|
|
53842a471e | ||
|
|
86a82d3039 | ||
|
|
2b78e74b4e | ||
|
|
db396dd739 | ||
|
|
3213db54d6 | ||
|
|
97a3f145a1 | ||
|
|
2fbe813770 | ||
|
|
23a23c6419 | ||
|
|
63eb1aac69 | ||
|
|
205bd19d3f | ||
|
|
950f062041 | ||
|
|
3476a556dd | ||
|
|
d2ef9cef10 | ||
|
|
0021c93fef | ||
|
|
dcf7064460 | ||
|
|
bebd3570ee | ||
|
|
7cfee1616a | ||
|
|
4b0a7c5d1d | ||
|
|
5df1a262ad | ||
|
|
84fddba2e1 | ||
|
|
8b0774b519 | ||
|
|
e2f8b28f56 | ||
|
|
3080f95a4f | ||
|
|
cc57908c03 | ||
|
|
4df4f218cf | ||
|
|
c2f71ee86b | ||
|
|
9f7e601110 | ||
|
|
3755031c18 | ||
|
|
b628477af1 | ||
|
|
dd455a6361 | ||
|
|
8c70682d6b | ||
|
|
742b3c69bb | ||
|
|
1593b784a7 | ||
|
|
db00ec8b32 | ||
|
|
3ca40b5dea | ||
|
|
378775992f | ||
|
|
319400ea23 | ||
|
|
289a03f9d7 | ||
|
|
a0710f4c70 | ||
|
|
737be1a0c1 | ||
|
|
1673098126 | ||
|
|
422bafd66a | ||
|
|
c0aab820ed | ||
|
|
3321cffe08 | ||
|
|
f2772518b0 | ||
|
|
76ef675eb2 | ||
|
|
e5fd4d929f | ||
|
|
2e42c7be1f | ||
|
|
95b031ee04 | ||
|
|
4d97a714a9 | ||
|
|
aa182f64b4 | ||
|
|
4e870302b1 | ||
|
|
098c89b5f5 | ||
|
|
70c45f8cd8 | ||
|
|
12d526a05c | ||
|
|
2fd8ad08c0 | ||
|
|
15e1d46125 | ||
|
|
73fcd42403 | ||
|
|
263e90e772 | ||
|
|
81770f094d | ||
|
|
dd5ee81089 | ||
|
|
a07e382617 | ||
|
|
4dc682313f | ||
|
|
5616ddf00e | ||
|
|
fcb23a4cd2 | ||
|
|
a8f60ff6b1 | ||
|
|
a206c8301d | ||
|
|
ee0de01480 | ||
|
|
887b05e6ed | ||
|
|
5d68e8803f | ||
|
|
dedd1d3dc0 | ||
|
|
d9ae36ec40 | ||
|
|
b9ac3fef8f | ||
|
|
f5faa4858c | ||
|
|
bc5e444d04 | ||
|
|
3a4b63f37e | ||
|
|
a1a128db19 | ||
|
|
d502b9282a | ||
|
|
fee8e41466 | ||
|
|
37260f17ff | ||
|
|
2da9d49478 | ||
|
|
7196a2d58e | ||
|
|
a056f6521c | ||
|
|
de16db9b72 | ||
|
|
b4ea483c4e | ||
|
|
7843bcca8d | ||
|
|
76ea5596ff | ||
|
|
f9318e8c93 | ||
|
|
71253ec6f9 | ||
|
|
0fa075b879 | ||
|
|
6d080afd22 | ||
|
|
a67e2a0a00 | ||
|
|
f387508666 | ||
|
|
632b1fb702 | ||
|
|
d84870d7a5 | ||
|
|
584b492e70 | ||
|
|
b79c9cb9bf | ||
|
|
cf5ccede50 | ||
|
|
23622a9739 | ||
|
|
7ba5a6ab75 | ||
|
|
be3d261e11 | ||
|
|
eeb061ef44 | ||
|
|
67b2c320a1 | ||
|
|
1d151885be | ||
|
|
e836024dd9 | ||
|
|
638455ed92 | ||
|
|
b2a74883e9 | ||
|
|
6875e5e16f | ||
|
|
a5717a3f8d | ||
|
|
804fcfebd1 | ||
|
|
b37ee5684a | ||
|
|
0573b63b4c | ||
|
|
29c21d7867 | ||
|
|
478eb893a9 | ||
|
|
eae29f8c48 | ||
|
|
4004cb63a5 | ||
|
|
aafdf4adb0 | ||
|
|
debac5dd0b | ||
|
|
ce06ed4bb1 | ||
|
|
939365c234 | ||
|
|
c60fd0715d | ||
|
|
c83e1c7b6d | ||
|
|
aabbd8f1a1 | ||
|
|
60d8a213a5 | ||
|
|
17cc8fdbb3 | ||
|
|
27c1019385 | ||
|
|
3a0ee6ae91 | ||
|
|
5eff375094 | ||
|
|
2df643ba18 | ||
|
|
68af2d6e2a | ||
|
|
dfb6c20493 | ||
|
|
4e9d414fde | ||
|
|
c0ec0652ae | ||
|
|
f8c3cea09b | ||
|
|
0b847e553c | ||
|
|
c2663258be | ||
|
|
d4654d0062 | ||
|
|
ffd4f9d959 | ||
|
|
292161865d | ||
|
|
35808390ee | ||
|
|
1678c07548 | ||
|
|
173a58d36a | ||
|
|
f31e309770 | ||
|
|
ffe1036e15 | ||
|
|
d3b113d2e1 | ||
|
|
2aa9bbbac2 | ||
|
|
d5e57a8ea4 | ||
|
|
5c0e0be639 | ||
|
|
a74d40f1e5 | ||
|
|
79d4ec1a48 | ||
|
|
4db22be8ba | ||
|
|
dc58a4d648 | ||
|
|
2809ec780a | ||
|
|
2d98392bf5 | ||
|
|
df6b442816 | ||
|
|
4b22fc8d1b | ||
|
|
fb6f9c1c13 | ||
|
|
66ae7f3ec0 | ||
|
|
0bea31d2ef | ||
|
|
e5b0487fef | ||
|
|
9117722740 | ||
|
|
baea0a6bf7 | ||
|
|
17347d43de | ||
|
|
794ca9f604 | ||
|
|
15f6cabf83 | ||
|
|
e53d102a2c | ||
|
|
33e7b8e373 | ||
|
|
9b8e3d437e | ||
|
|
a2f0f2547d | ||
|
|
57d5426eed | ||
|
|
3e3a415457 | ||
|
|
deba039d6d | ||
|
|
ee4295c889 | ||
|
|
f5e67a12f9 | ||
|
|
6b56bdd74f | ||
|
|
ba6e3e3da6 | ||
|
|
a8e63ff03d | ||
|
|
052ef9fcb8 | ||
|
|
d07b12dc7d | ||
|
|
32e4f2d3b1 | ||
|
|
2cb1483d54 | ||
|
|
6c6350f18b | ||
|
|
eae24373e6 | ||
|
|
034a47a6cf | ||
|
|
51c1165f26 | ||
|
|
0b45ffeada | ||
|
|
b7b243a13d | ||
|
|
18e8ce1eb0 | ||
|
|
994f309465 | ||
|
|
02d42a1475 | ||
|
|
d099878385 | ||
|
|
6a061cbe81 | ||
|
|
c864a75903 | ||
|
|
d6fdf86acd | ||
|
|
55597b8398 | ||
|
|
fa14fd02e7 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,3 +6,4 @@ pkg
|
||||
.rvmrc
|
||||
.ruby-version
|
||||
Gemfile.lock
|
||||
.bundle
|
||||
|
||||
125
.rubocop.yml
Normal file
125
.rubocop.yml
Normal file
@@ -0,0 +1,125 @@
|
||||
inherit_from: ./.rubocop_todo.yml
|
||||
|
||||
AllCops:
|
||||
Exclude:
|
||||
- 'performance/shopify/*'
|
||||
- 'pkg/**'
|
||||
|
||||
Metrics/BlockNesting:
|
||||
Max: 3
|
||||
Exclude:
|
||||
- 'lib/liquid/block_body.rb'
|
||||
|
||||
Metrics/ModuleLength:
|
||||
Enabled: false
|
||||
|
||||
Metrics/ClassLength:
|
||||
Enabled: false
|
||||
|
||||
Lint/AssignmentInCondition:
|
||||
Enabled: false
|
||||
|
||||
Lint/AmbiguousOperator:
|
||||
Enabled: false
|
||||
|
||||
Lint/AmbiguousRegexpLiteral:
|
||||
Enabled: false
|
||||
|
||||
Lint/ParenthesesAsGroupedExpression:
|
||||
Enabled: false
|
||||
|
||||
Lint/UnusedBlockArgument:
|
||||
Enabled: false
|
||||
|
||||
Lint/EndAlignment:
|
||||
AlignWith: variable
|
||||
|
||||
Lint/UnusedMethodArgument:
|
||||
Enabled: false
|
||||
|
||||
Style/SingleLineBlockParams:
|
||||
Enabled: false
|
||||
|
||||
Style/DoubleNegation:
|
||||
Enabled: false
|
||||
|
||||
Style/StringLiteralsInInterpolation:
|
||||
Enabled: false
|
||||
|
||||
Style/AndOr:
|
||||
Enabled: false
|
||||
|
||||
Style/SignalException:
|
||||
Enabled: false
|
||||
|
||||
Style/StringLiterals:
|
||||
Enabled: false
|
||||
|
||||
Style/BracesAroundHashParameters:
|
||||
Enabled: false
|
||||
|
||||
Style/NumericLiterals:
|
||||
Enabled: false
|
||||
|
||||
Style/SpaceInsideBrackets:
|
||||
Enabled: false
|
||||
|
||||
Style/SpaceBeforeBlockBraces:
|
||||
Enabled: false
|
||||
|
||||
Style/Documentation:
|
||||
Enabled: false
|
||||
|
||||
Style/ClassAndModuleChildren:
|
||||
Enabled: false
|
||||
|
||||
Style/TrailingComma:
|
||||
Enabled: false
|
||||
|
||||
Style/IndentHash:
|
||||
EnforcedStyle: consistent
|
||||
|
||||
Style/FormatString:
|
||||
Enabled: false
|
||||
|
||||
Style/AlignParameters:
|
||||
EnforcedStyle: with_fixed_indentation
|
||||
|
||||
Style/MultilineOperationIndentation:
|
||||
EnforcedStyle: indented
|
||||
|
||||
Style/IfUnlessModifier:
|
||||
Enabled: false
|
||||
|
||||
Style/RaiseArgs:
|
||||
Enabled: false
|
||||
|
||||
Style/DeprecatedHashMethods:
|
||||
Enabled: false
|
||||
|
||||
Style/RegexpLiteral:
|
||||
Enabled: false
|
||||
|
||||
Style/SymbolLiteral:
|
||||
Enabled: false
|
||||
|
||||
Performance/Count:
|
||||
Enabled: false
|
||||
|
||||
Style/ConstantName:
|
||||
Enabled: false
|
||||
|
||||
Style/CaseIndentation:
|
||||
Enabled: false
|
||||
|
||||
Style/ClassVars:
|
||||
Enabled: false
|
||||
|
||||
Style/PerlBackrefs:
|
||||
Enabled: false
|
||||
|
||||
Style/TrivialAccessors:
|
||||
AllowPredicates: true
|
||||
|
||||
Style/WordArray:
|
||||
Enabled: false
|
||||
72
.rubocop_todo.yml
Normal file
72
.rubocop_todo.yml
Normal file
@@ -0,0 +1,72 @@
|
||||
# This configuration was generated by `rubocop --auto-gen-config`
|
||||
# on 2015-06-08 18:16:16 +0000 using RuboCop version 0.32.0.
|
||||
# The point is for the user to remove these configuration records
|
||||
# one by one as the offenses are removed from the code base.
|
||||
# Note that changes in the inspected code, or installation of new
|
||||
# versions of RuboCop, may require this file to be generated again.
|
||||
|
||||
# Offense count: 5
|
||||
Lint/NestedMethodDefinition:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 53
|
||||
Metrics/AbcSize:
|
||||
Max: 58
|
||||
|
||||
# Offense count: 12
|
||||
Metrics/CyclomaticComplexity:
|
||||
Max: 15
|
||||
|
||||
# Offense count: 553
|
||||
# Configuration parameters: AllowURI, URISchemes.
|
||||
Metrics/LineLength:
|
||||
Max: 294
|
||||
|
||||
# Offense count: 77
|
||||
# Configuration parameters: CountComments.
|
||||
Metrics/MethodLength:
|
||||
Max: 46
|
||||
|
||||
# Offense count: 6
|
||||
Metrics/PerceivedComplexity:
|
||||
Max: 13
|
||||
|
||||
# Offense count: 1
|
||||
Style/AccessorMethodName:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 1
|
||||
# Cop supports --auto-correct.
|
||||
Style/ClosingParenthesisIndentation:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 3
|
||||
# Configuration parameters: MinBodyLength.
|
||||
Style/GuardClause:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 4
|
||||
# Configuration parameters: EnforcedStyle, SupportedStyles.
|
||||
Style/MethodName:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 1
|
||||
Style/MultilineBlockChain:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 2
|
||||
# Configuration parameters: EnforcedStyle, MinBodyLength, SupportedStyles.
|
||||
Style/Next:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 7
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: AllowAsExpressionSeparator.
|
||||
Style/Semicolon:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 1
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: MaxLineLength.
|
||||
Style/WhileUntilModifier:
|
||||
Enabled: false
|
||||
19
.travis.yml
19
.travis.yml
@@ -1,16 +1,25 @@
|
||||
language: ruby
|
||||
|
||||
rvm:
|
||||
- 1.9
|
||||
- 2.0
|
||||
- 2.1
|
||||
- jruby-19mode
|
||||
- 2.2
|
||||
- ruby-head
|
||||
- jruby-head
|
||||
- rbx-2
|
||||
# - rbx-2
|
||||
|
||||
sudo: false
|
||||
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- libgmp3-dev
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- rvm: rbx-2
|
||||
- rvm: jruby-head
|
||||
|
||||
script: "rake test"
|
||||
script: "bundle exec rake"
|
||||
|
||||
notifications:
|
||||
disable: true
|
||||
|
||||
@@ -4,23 +4,22 @@
|
||||
|
||||
* Bugfixes
|
||||
* 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
|
||||
|
||||
* Code which introduces considerable performance degrations
|
||||
* Code which 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 which can easily be implemented on top of Liquid (for example as a custom filter or custom filesystem)
|
||||
* Code which comes without tests
|
||||
* Code which breaks existing tests
|
||||
* Code that introduces considerable performance degrations
|
||||
* Code that touches performance-critical parts of Liquid and comes without benchmarks
|
||||
* Features that are not important for most people (we want to keep the core Liquid code small and tidy)
|
||||
* Features that can easily be implemented on top of Liquid (for example as a custom filter or custom filesystem)
|
||||
* Code that does not include tests
|
||||
* Code that breaks existing tests
|
||||
|
||||
## Workflow
|
||||
|
||||
* Fork the Liquid repository
|
||||
* Create a new branch in your fork
|
||||
* If it makes sense, add tests for your code and run a performance benchmark
|
||||
* Make sure all tests pass
|
||||
* If it makes sense, add tests for your code and/or run a performance benchmark
|
||||
* Make sure all tests pass (`bundle exec rake`)
|
||||
* Create a pull request
|
||||
* 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.
|
||||
|
||||
|
||||
6
Gemfile
6
Gemfile
@@ -5,4 +5,10 @@ gem 'stackprof', platforms: :mri_21
|
||||
|
||||
group :test do
|
||||
gem 'spy', '0.4.1'
|
||||
gem 'benchmark-ips'
|
||||
gem 'rubocop', '0.34.2'
|
||||
|
||||
platform :mri do
|
||||
gem 'liquid-c', github: 'Shopify/liquid-c', ref: '2570693d8d03faa0df9160ec74348a7149436df3'
|
||||
end
|
||||
end
|
||||
|
||||
185
History.md
185
History.md
@@ -1,67 +1,150 @@
|
||||
# Liquid Version History
|
||||
# Liquid Change Log
|
||||
|
||||
## 3.0.0 / not yet released / branch "master"
|
||||
## 4.0.0 / not yet released / branch "master"
|
||||
|
||||
* ...
|
||||
* Optimize variable parsing to avoid repeated regex evaluation during template rendering #383 [Jason Hiltz-Laforge, jasonhl]
|
||||
* Optimize checking for block interrupts to reduce object allocation #380 [Jason Hiltz-Laforge, jasonhl]
|
||||
* Properly set context rethrow_errors on render! #349 [Thierry Joyal, tjoyal]
|
||||
* Fix broken rendering of variables which are equal to false, see #345 [Florian Weingarten, fw42]
|
||||
* Remove ActionView template handler [Dylan Thacker-Smith, dylanahsmith]
|
||||
* Freeze lots of string literals for new Ruby 2.1 optimization, see #297 [Florian Weingarten, fw42]
|
||||
* Allow newlines in tags and variables, see #324 [Dylan Thacker-Smith, dylanahsmith]
|
||||
* Tag#parse is called after initialize, which now takes options instead of tokens as the 3rd argument. See #321 [Dylan Thacker-Smith, dylanahsmith]
|
||||
* Raise `Liquid::ArgumentError` instead of `::ArgumentError` when filter has wrong number of arguments #309 [Bogdan Gusiev, bogdan]
|
||||
* Add a to_s default for liquid drops, see #306 [Adam Doeler, releod]
|
||||
* Add strip, lstrip, and rstrip to standard filters [Florian Weingarten, fw42]
|
||||
* Make if, for & case tags return complete and consistent nodelists, see #250 [Nick Jones, dntj]
|
||||
* Prevent arbitrary method invocation on condition objects, see #274 [Dylan Thacker-Smith, dylanahsmith]
|
||||
* Don't call to_sym when creating conditions for security reasons, see #273 [Bouke van der Bijl, bouk]
|
||||
* Fix resource counting bug with respond_to?(:length), see #263 [Florian Weingarten, fw42]
|
||||
* Allow specifying custom patterns for template filenames, see #284 [Andrei Gladkyi, agladkyi]
|
||||
* Allow drops to optimize loading a slice of elements, see #282 [Tom Burns, boourns]
|
||||
* Support for passing variables to snippets in subdirs, see #271 [Joost Hietbrink, joost]
|
||||
* Add a class cache to avoid runtime extend calls, see #249 [James Tucker, raggi]
|
||||
* Remove some legacy Ruby 1.8 compatibility code, see #276 [Florian Weingarten, fw42]
|
||||
* Add default filter to standard filters, see #267 [Derrick Reimer, djreimer]
|
||||
* Add optional strict parsing and warn parsing, see #235 [Tristan Hume, trishume]
|
||||
* Add I18n syntax error translation, see #241 [Simon Hørup Eskildsen, Sirupsen]
|
||||
* Make sort filter work on enumerable drops, see #239 [Florian Weingarten, fw42]
|
||||
* Fix clashing method names in enumerable drops, see #238 [Florian Weingarten, fw42]
|
||||
* Make map filter work on enumerable drops, see #233 [Florian Weingarten, fw42]
|
||||
* Improved whitespace stripping for blank blocks, related to #216 [Florian Weingarten, fw42]
|
||||
### Changed
|
||||
* 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]
|
||||
* Rename deprecated "has_key?" and "has_interrupt?" methods (#593) [Florian Weingarten]
|
||||
* Include template name with line numbers in render errors (574) [Dylan Thacker-Smith]
|
||||
* Add sort_natural filter (#554) [Martin Hanzel]
|
||||
* Add forloop.parentloop as a reference to the parent loop (#520) [Justin Li]
|
||||
* Block parsing moved to BlockBody class (#458) [Dylan Thacker-Smith]
|
||||
* Add concat filter to concatenate arrays (#429) [Diogo Beato]
|
||||
* 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]
|
||||
* Remove support for `liquid_methods`
|
||||
* Liquid::Template.register_filter raises when the module overrides registered public methods as private or protected (#705) [Gaurav Chande]
|
||||
|
||||
## 2.6.0 / 2013-11-25 / branch "2.6-stable"
|
||||
### Fixed
|
||||
* 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 when "blank" and "empty" are used as variable names (#592) [Florian Weingarten]
|
||||
* Fix condition parse order in strict mode (#569) [Justin Li]
|
||||
* Fix naming of the "context variable" when dynamically including a template (#559) [Justin Li]
|
||||
* Gracefully accept empty strings in the date filter (#555) [Loren Hale]
|
||||
* Fix capturing into variables with a hyphen in the name (#505) [Florian Weingarten]
|
||||
* Fix case sensitivity regression in date standard filter (#499) [Kelley Reynolds]
|
||||
* Disallow filters with no variable in strict mode (#475) [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]
|
||||
|
||||
## 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]
|
||||
|
||||
## 3.0.2 / 2015-04-24
|
||||
|
||||
* Expose VariableLookup private members (#551) [Justin Li]
|
||||
* Documentation fixes
|
||||
|
||||
## 3.0.1 / 2015-01-23
|
||||
|
||||
* Remove duplicate `index0` key in TableRow tag (#502) [Alfred Xing]
|
||||
|
||||
## 3.0.0 / 2014-11-12
|
||||
|
||||
* Removed Block#end_tag. Instead, override parse with `super` followed by your code. See #446 [Dylan Thacker-Smith]
|
||||
* Fixed condition with wrong data types (#423) [Bogdan Gusiev]
|
||||
* Add url_encode to standard filters (#421) [Derrick Reimer]
|
||||
* Add uniq to standard filters [Florian Weingarten]
|
||||
* Add exception_handler feature (#397) and #254 [Bogdan Gusiev, Florian Weingarten]
|
||||
* Optimize variable parsing to avoid repeated regex evaluation during template rendering #383 [Jason Hiltz-Laforge]
|
||||
* Optimize checking for block interrupts to reduce object allocation #380 [Jason Hiltz-Laforge]
|
||||
* Properly set context rethrow_errors on render! #349 [Thierry Joyal]
|
||||
* Fix broken rendering of variables which are equal to false (#345) [Florian Weingarten]
|
||||
* Remove ActionView template handler [Dylan Thacker-Smith]
|
||||
* Freeze lots of string literals for new Ruby 2.1 optimization (#297) [Florian Weingarten]
|
||||
* Allow newlines in tags and variables (#324) [Dylan Thacker-Smith]
|
||||
* Tag#parse is called after initialize, which now takes options instead of tokens as the 3rd argument. See #321 [Dylan Thacker-Smith]
|
||||
* Raise `Liquid::ArgumentError` instead of `::ArgumentError` when filter has wrong number of arguments #309 [Bogdan Gusiev]
|
||||
* Add a to_s default for liquid drops (#306) [Adam Doeler]
|
||||
* Add strip, lstrip, and rstrip to standard filters [Florian Weingarten]
|
||||
* Make if, for & case tags return complete and consistent nodelists (#250) [Nick Jones]
|
||||
* Prevent arbitrary method invocation on condition objects (#274) [Dylan Thacker-Smith]
|
||||
* Don't call to_sym when creating conditions for security reasons (#273) [Bouke van der Bijl]
|
||||
* Fix resource counting bug with respond_to?(:length) (#263) [Florian Weingarten]
|
||||
* Allow specifying custom patterns for template filenames (#284) [Andrei Gladkyi]
|
||||
* Allow drops to optimize loading a slice of elements (#282) [Tom Burns]
|
||||
* Support for passing variables to snippets in subdirs (#271) [Joost Hietbrink]
|
||||
* Add a class cache to avoid runtime extend calls (#249) [James Tucker]
|
||||
* Remove some legacy Ruby 1.8 compatibility code (#276) [Florian Weingarten]
|
||||
* Add default filter to standard filters (#267) [Derrick Reimer]
|
||||
* Add optional strict parsing and warn parsing (#235) [Tristan Hume]
|
||||
* Add I18n syntax error translation (#241) [Simon Hørup Eskildsen, Sirupsen]
|
||||
* Make sort filter work on enumerable drops (#239) [Florian Weingarten]
|
||||
* Fix clashing method names in enumerable drops (#238) [Florian Weingarten]
|
||||
* Make map filter work on enumerable drops (#233) [Florian Weingarten]
|
||||
* Improved whitespace stripping for blank blocks, related to #216 [Florian Weingarten]
|
||||
|
||||
## 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):
|
||||
* Don't call to_sym when creating conditions for security reasons (#273) [Bouke van der Bijl]
|
||||
* Prevent arbitrary method invocation on condition objects (#274) [Dylan Thacker-Smith]
|
||||
|
||||
## 2.6.0 / 2013-11-25
|
||||
|
||||
IMPORTANT: Liquid 2.6 is going to be the last version of Liquid which maintains explicit Ruby 1.8 compatability.
|
||||
The following releases will only be tested against Ruby 1.9 and Ruby 2.0 and are likely to break on Ruby 1.8.
|
||||
|
||||
* Bugfix for #106: fix example servlet [gnowoel]
|
||||
* Bugfix for #97: strip_html filter supports multi-line tags [Jo Liss, joliss]
|
||||
* Bugfix for #114: strip_html filter supports style tags [James Allardice, jamesallardice]
|
||||
* Bugfix for #117: 'now' support for date filter in Ruby 1.9 [Notre Dame Webgroup, ndwebgroup]
|
||||
* Bugfix for #166: truncate filter on UTF-8 strings with Ruby 1.8 [Florian Weingarten, fw42]
|
||||
* Bugfix for #204: 'raw' parsing bug [Florian Weingarten, fw42]
|
||||
* Bugfix for #150: 'for' parsing bug [Peter Schröder, phoet]
|
||||
* Bugfix for #126: Strip CRLF in strip_newline [Peter Schröder, phoet]
|
||||
* Bugfix for #174, "can't convert Fixnum into String" for "replace" [wǒ_is神仙, jsw0528]
|
||||
* Allow a Liquid::Drop to be passed into Template#render [Daniel Huckstep, darkhelmet]
|
||||
* Resource limits [Florian Weingarten, fw42]
|
||||
* Add reverse filter [Jay Strybis, unreal]
|
||||
* Bugfix for #97: strip_html filter supports multi-line tags [Jo Liss]
|
||||
* Bugfix for #114: strip_html filter supports style tags [James Allardice]
|
||||
* Bugfix for #117: 'now' support for date filter in Ruby 1.9 [Notre Dame Webgroup]
|
||||
* Bugfix for #166: truncate filter on UTF-8 strings with Ruby 1.8 [Florian Weingarten]
|
||||
* Bugfix for #204: 'raw' parsing bug [Florian Weingarten]
|
||||
* Bugfix for #150: 'for' parsing bug [Peter Schröder]
|
||||
* Bugfix for #126: Strip CRLF in strip_newline [Peter Schröder]
|
||||
* Bugfix for #174, "can't convert Fixnum into String" for "replace" [jsw0528]
|
||||
* Allow a Liquid::Drop to be passed into Template#render [Daniel Huckstep]
|
||||
* Resource limits [Florian Weingarten]
|
||||
* Add reverse filter [Jay Strybis]
|
||||
* Add utf-8 support
|
||||
* Use array instead of Hash to keep the registered filters [Tasos Stathopoulos, astathopoulos]
|
||||
* Cache tokenized partial templates [Tom Burns, boourns]
|
||||
* Avoid warnings in Ruby 1.9.3 [Marcus Stollsteimer, stomar]
|
||||
* Better documentation for 'include' tag (closes #163) [Peter Schröder, phoet]
|
||||
* Use of BigDecimal on filters to have better precision (closes #155) [Arthur Nogueira Neves, arthurnn]
|
||||
* Use array instead of Hash to keep the registered filters [Tasos Stathopoulos]
|
||||
* Cache tokenized partial templates [Tom Burns]
|
||||
* Avoid warnings in Ruby 1.9.3 [Marcus Stollsteimer]
|
||||
* Better documentation for 'include' tag (closes #163) [Peter Schröder]
|
||||
* Use of BigDecimal on filters to have better precision (closes #155) [Arthur Nogueira Neves]
|
||||
|
||||
## 2.5.4 / 2013-11-11 / branch "2.5-stable"
|
||||
## 2.5.5 / 2014-01-10 / branch "2-5-stable"
|
||||
|
||||
* Fix "can't convert Fixnum into String" for "replace", see #173, [wǒ_is神仙, jsw0528]
|
||||
Security fix, cherry-picked from master (4e14a65):
|
||||
* Don't call to_sym when creating conditions for security reasons (#273) [Bouke van der Bijl]
|
||||
* Prevent arbitrary method invocation on condition objects (#274) [Dylan Thacker-Smith]
|
||||
|
||||
## 2.5.4 / 2013-11-11
|
||||
|
||||
* Fix "can't convert Fixnum into String" for "replace" (#173), [jsw0528]
|
||||
|
||||
## 2.5.3 / 2013-10-09
|
||||
|
||||
* #232, #234, #237: Fix map filter bugs [Florian Weingarten, fw42]
|
||||
* #232, #234, #237: Fix map filter bugs [Florian Weingarten]
|
||||
|
||||
## 2.5.2 / 2013-09-03 / deleted
|
||||
|
||||
@@ -69,7 +152,7 @@ Yanked from rubygems, as it contained too many changes that broke compatibility.
|
||||
|
||||
## 2.5.1 / 2013-07-24
|
||||
|
||||
* #230: Fix security issue with map filter, Use invoke_drop in map filter [Florian Weingarten, fw42]
|
||||
* #230: Fix security issue with map filter, Use invoke_drop in map filter [Florian Weingarten]
|
||||
|
||||
## 2.5.0 / 2013-03-06
|
||||
|
||||
|
||||
35
README.md
35
README.md
@@ -1,5 +1,5 @@
|
||||
[](http://travis-ci.org/Shopify/liquid)
|
||||
[](http://inch-ci.org/github/Shopify/liquid)
|
||||
[](http://travis-ci.org/Shopify/liquid)
|
||||
[](http://inch-ci.org/github/Shopify/liquid)
|
||||
|
||||
# Liquid template engine
|
||||
|
||||
@@ -73,3 +73,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 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
|
||||
```
|
||||
|
||||
34
Rakefile
34
Rakefile
@@ -3,7 +3,7 @@ require 'rake/testtask'
|
||||
$LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
|
||||
require "liquid/version"
|
||||
|
||||
task :default => 'test'
|
||||
task default: [:rubocop, :test]
|
||||
|
||||
desc 'run test suite with default parser'
|
||||
Rake::TestTask.new(:base_test) do |t|
|
||||
@@ -18,25 +18,43 @@ task :warn_test do
|
||||
Rake::Task['base_test'].invoke
|
||||
end
|
||||
|
||||
task :rubocop do
|
||||
require 'rubocop/rake_task'
|
||||
RuboCop::RakeTask.new
|
||||
end
|
||||
|
||||
desc 'runs test suite with both strict and lax parsers'
|
||||
task :test do
|
||||
ENV['LIQUID_PARSER_MODE'] = 'lax'
|
||||
Rake::Task['base_test'].invoke
|
||||
|
||||
ENV['LIQUID_PARSER_MODE'] = 'strict'
|
||||
Rake::Task['base_test'].reenable
|
||||
Rake::Task['base_test'].invoke
|
||||
|
||||
if RUBY_ENGINE == 'ruby'
|
||||
ENV['LIQUID-C'] = '1'
|
||||
|
||||
ENV['LIQUID_PARSER_MODE'] = 'lax'
|
||||
Rake::Task['base_test'].reenable
|
||||
Rake::Task['base_test'].invoke
|
||||
|
||||
ENV['LIQUID_PARSER_MODE'] = 'strict'
|
||||
Rake::Task['base_test'].reenable
|
||||
Rake::Task['base_test'].invoke
|
||||
end
|
||||
end
|
||||
|
||||
task :gem => :build
|
||||
task gem: :build
|
||||
task :build do
|
||||
system "gem build liquid.gemspec"
|
||||
end
|
||||
|
||||
task :install => :build do
|
||||
task install: :build do
|
||||
system "gem install liquid-#{Liquid::VERSION}.gem"
|
||||
end
|
||||
|
||||
task :release => :build do
|
||||
task release: :build do
|
||||
system "git tag -a v#{Liquid::VERSION} -m 'Tagging #{Liquid::VERSION}'"
|
||||
system "git push --tags"
|
||||
system "gem push liquid-#{Liquid::VERSION}.gem"
|
||||
@@ -44,7 +62,6 @@ task :release => :build do
|
||||
end
|
||||
|
||||
namespace :benchmark do
|
||||
|
||||
desc "Run the liquid benchmark with lax parsing"
|
||||
task :run do
|
||||
ruby "./performance/benchmark.rb lax"
|
||||
@@ -56,9 +73,7 @@ namespace :benchmark do
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
namespace :profile do
|
||||
|
||||
desc "Run the liquid profile/performance coverage"
|
||||
task :run do
|
||||
ruby "./performance/profile.rb"
|
||||
@@ -68,10 +83,13 @@ namespace :profile do
|
||||
task :strict do
|
||||
ruby "./performance/profile.rb strict"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
desc "Run example"
|
||||
task :example do
|
||||
ruby "-w -d -Ilib example/server/server.rb"
|
||||
end
|
||||
|
||||
task :console do
|
||||
exec 'irb -I lib -r liquid'
|
||||
end
|
||||
|
||||
3
circle.yml
Normal file
3
circle.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
machine:
|
||||
ruby:
|
||||
version: ruby-2.0
|
||||
@@ -4,7 +4,7 @@ module ProductsFilter
|
||||
end
|
||||
|
||||
def prettyprint(text)
|
||||
text.gsub( /\*(.*)\*/, '<b>\1</b>' )
|
||||
text.gsub(/\*(.*)\*/, '<b>\1</b>')
|
||||
end
|
||||
|
||||
def count(array)
|
||||
@@ -17,25 +17,28 @@ module ProductsFilter
|
||||
end
|
||||
|
||||
class Servlet < LiquidServlet
|
||||
|
||||
def index
|
||||
{ 'date' => Time.now }
|
||||
end
|
||||
|
||||
def products
|
||||
{ 'products' => products_list, 'description' => description, 'section' => 'Snowboards', 'cool_products' => true}
|
||||
{ 'products' => products_list, 'more_products' => more_products_list, 'description' => description, 'section' => 'Snowboards', 'cool_products' => true }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def products_list
|
||||
[{'name' => 'Arbor Draft', 'price' => 39900, 'description' => 'the *arbor draft* is a excellent product' },
|
||||
{'name' => 'Arbor Element', 'price' => 40000, 'description' => 'the *arbor element* rocks for freestyling'},
|
||||
{'name' => 'Arbor Diamond', 'price' => 59900, 'description' => 'the *arbor diamond* is a made up product because im obsessed with arbor and have no creativity'}]
|
||||
[{ 'name' => 'Arbor Draft', 'price' => 39900, 'description' => 'the *arbor draft* is a excellent product' },
|
||||
{ 'name' => 'Arbor Element', 'price' => 40000, 'description' => 'the *arbor element* rocks for freestyling' },
|
||||
{ 'name' => 'Arbor Diamond', 'price' => 59900, 'description' => 'the *arbor diamond* is a made up product because im obsessed with arbor and have no creativity' }]
|
||||
end
|
||||
|
||||
def more_products_list
|
||||
[{ 'name' => 'Arbor Catalyst', 'price' => 39900, 'description' => 'the *arbor catalyst* is an advanced drop-through for freestyle and flatground performance and versatility' },
|
||||
{ 'name' => 'Arbor Fish', 'price' => 40000, 'description' => 'the *arbor fish* is a compact pin that features an extended wheelbase and time-honored teardrop shape' }]
|
||||
end
|
||||
|
||||
def description
|
||||
"List of Products ~ This is a list of products with price and description."
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet
|
||||
|
||||
def do_GET(req, res)
|
||||
handle(:get, req, res)
|
||||
end
|
||||
@@ -11,7 +10,8 @@ class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet
|
||||
private
|
||||
|
||||
def handle(type, req, res)
|
||||
@request, @response = req, res
|
||||
@request = req
|
||||
@response = res
|
||||
|
||||
@request.path_info =~ /(\w+)\z/
|
||||
@action = $1 || 'index'
|
||||
@@ -19,10 +19,10 @@ class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet
|
||||
|
||||
@response['Content-Type'] = "text/html"
|
||||
@response.status = 200
|
||||
@response.body = Liquid::Template.parse(read_template).render(@assigns, :filters => [ProductsFilter])
|
||||
@response.body = Liquid::Template.parse(read_template).render(@assigns, filters: [ProductsFilter])
|
||||
end
|
||||
|
||||
def read_template(filename = @action)
|
||||
File.read( File.dirname(__FILE__) + "/templates/#{filename}.liquid" )
|
||||
File.read("#{__dir__}/templates/#{filename}.liquid")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
require 'webrick'
|
||||
require 'rexml/document'
|
||||
|
||||
DIR = File.expand_path(File.dirname(__FILE__))
|
||||
|
||||
require DIR + '/../../lib/liquid'
|
||||
require DIR + '/liquid_servlet'
|
||||
require DIR + '/example_servlet'
|
||||
require_relative '../../lib/liquid'
|
||||
require_relative 'liquid_servlet'
|
||||
require_relative 'example_servlet'
|
||||
|
||||
# Setup webrick
|
||||
server = WEBrick::HTTPServer.new( :Port => ARGV[1] || 3000 )
|
||||
server = WEBrick::HTTPServer.new(Port: ARGV[1] || 3000)
|
||||
server.mount('/', Servlet)
|
||||
trap("INT"){ server.shutdown }
|
||||
server.start
|
||||
|
||||
@@ -16,12 +16,12 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
{% assign all_products = products | concat: more_products %}
|
||||
<h1>{{ description | split: '~' | first }}</h1>
|
||||
|
||||
<h2>{{ description | split: '~' | last }}</h2>
|
||||
|
||||
<h2>There are currently {{products | count}} products in the {{section}} catalog</h2>
|
||||
<h2>There are currently {{all_products | count}} products in the {{section}} catalog</h2>
|
||||
|
||||
{% if cool_products %}
|
||||
Cool products :)
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
<ul id="products">
|
||||
|
||||
{% for product in products %}
|
||||
{% for product in all_products %}
|
||||
<li>
|
||||
<h2>{{product.name}}</h2>
|
||||
Only {{product.price | price }}
|
||||
|
||||
@@ -48,22 +48,31 @@ require 'liquid/lexer'
|
||||
require 'liquid/parser'
|
||||
require 'liquid/i18n'
|
||||
require 'liquid/drop'
|
||||
require 'liquid/tablerowloop_drop'
|
||||
require 'liquid/forloop_drop'
|
||||
require 'liquid/extensions'
|
||||
require 'liquid/errors'
|
||||
require 'liquid/interrupts'
|
||||
require 'liquid/strainer'
|
||||
require 'liquid/expression'
|
||||
require 'liquid/context'
|
||||
require 'liquid/parser_switching'
|
||||
require 'liquid/tag'
|
||||
require 'liquid/block'
|
||||
require 'liquid/block_body'
|
||||
require 'liquid/document'
|
||||
require 'liquid/variable'
|
||||
require 'liquid/variable_lookup'
|
||||
require 'liquid/range_lookup'
|
||||
require 'liquid/file_system'
|
||||
require 'liquid/resource_limits'
|
||||
require 'liquid/template'
|
||||
require 'liquid/standardfilters'
|
||||
require 'liquid/condition'
|
||||
require 'liquid/module_ex'
|
||||
require 'liquid/utils'
|
||||
require 'liquid/tokenizer'
|
||||
require 'liquid/parse_context'
|
||||
|
||||
# Load all the tags of the standard library
|
||||
#
|
||||
Dir[File.dirname(__FILE__) + '/liquid/tags/*.rb'].each { |f| require f }
|
||||
Dir["#{__dir__}/liquid/tags/*.rb"].each { |f| require f }
|
||||
|
||||
@@ -1,98 +1,39 @@
|
||||
module Liquid
|
||||
class Block < Tag
|
||||
FullToken = /\A#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
|
||||
ContentOfVariable = /\A#{VariableStart}(.*)#{VariableEnd}\z/om
|
||||
TAGSTART = "{%".freeze
|
||||
VARSTART = "{{".freeze
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
@block_delimiter = "end#{tag_name}"
|
||||
end
|
||||
@blank = true
|
||||
end
|
||||
|
||||
def parse(tokens)
|
||||
@body = BlockBody.new
|
||||
while parse_body(@body, tokens)
|
||||
end
|
||||
end
|
||||
|
||||
def render(context)
|
||||
@body.render(context)
|
||||
end
|
||||
|
||||
def blank?
|
||||
@blank
|
||||
end
|
||||
|
||||
def parse(tokens)
|
||||
@blank = true
|
||||
@nodelist ||= []
|
||||
@nodelist.clear
|
||||
|
||||
# All child tags of the current block.
|
||||
@children = []
|
||||
|
||||
while token = tokens.shift
|
||||
unless token.empty?
|
||||
case
|
||||
when token.start_with?(TAGSTART)
|
||||
if token =~ FullToken
|
||||
|
||||
# if we found the proper block delimiter just end parsing here and let the outer block
|
||||
# proceed
|
||||
if @block_delimiter == $1
|
||||
end_tag
|
||||
return
|
||||
end
|
||||
|
||||
# fetch the tag from registered blocks
|
||||
if tag = Template.tags[$1]
|
||||
new_tag = tag.parse($1, $2, tokens, @options)
|
||||
@blank &&= new_tag.blank?
|
||||
@nodelist << new_tag
|
||||
@children << new_tag
|
||||
else
|
||||
# this tag is not registered with the system
|
||||
# pass it to the current block for special handling or error reporting
|
||||
unknown_tag($1, $2, tokens)
|
||||
end
|
||||
else
|
||||
raise SyntaxError.new(options[:locale].t("errors.syntax.tag_termination".freeze, :token => token, :tag_end => TagEnd.inspect))
|
||||
end
|
||||
when token.start_with?(VARSTART)
|
||||
new_var = create_variable(token)
|
||||
@nodelist << new_var
|
||||
@children << new_var
|
||||
@blank = false
|
||||
else
|
||||
@nodelist << token
|
||||
@blank &&= (token =~ /\A\s*\z/)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Make sure that it's ok to end parsing in the current block.
|
||||
# Effectively this method will throw an exception unless the current block is
|
||||
# of type Document
|
||||
assert_missing_delimitation!
|
||||
def nodelist
|
||||
@body.nodelist
|
||||
end
|
||||
|
||||
# warnings of this block and all sub-tags
|
||||
def warnings
|
||||
all_warnings = []
|
||||
all_warnings.concat(@warnings) if @warnings
|
||||
|
||||
(@children || []).each do |node|
|
||||
all_warnings.concat(node.warnings || [])
|
||||
end
|
||||
|
||||
all_warnings
|
||||
end
|
||||
|
||||
def end_tag
|
||||
end
|
||||
|
||||
def unknown_tag(tag, params, tokens)
|
||||
def unknown_tag(tag, _params, _tokens)
|
||||
case tag
|
||||
when 'else'.freeze
|
||||
raise SyntaxError.new(options[:locale].t("errors.syntax.unexpected_else".freeze,
|
||||
:block_name => block_name))
|
||||
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_else".freeze,
|
||||
block_name: block_name))
|
||||
when 'end'.freeze
|
||||
raise SyntaxError.new(options[:locale].t("errors.syntax.invalid_delimiter".freeze,
|
||||
:block_name => block_name,
|
||||
:block_delimiter => @block_delimiter))
|
||||
raise SyntaxError.new(parse_context.locale.t("errors.syntax.invalid_delimiter".freeze,
|
||||
block_name: block_name,
|
||||
block_delimiter: block_delimiter))
|
||||
else
|
||||
raise SyntaxError.new(options[:locale].t("errors.syntax.unknown_tag".freeze, :tag => tag))
|
||||
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unknown_tag".freeze, tag: tag))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -100,58 +41,27 @@ module Liquid
|
||||
@tag_name
|
||||
end
|
||||
|
||||
def create_variable(token)
|
||||
token.scan(ContentOfVariable) do |content|
|
||||
return Variable.new(content.first, @options)
|
||||
end
|
||||
raise SyntaxError.new(options[:locale].t("errors.syntax.variable_termination".freeze, :token => token, :tag_end => VariableEnd.inspect))
|
||||
end
|
||||
|
||||
def render(context)
|
||||
render_all(@nodelist, context)
|
||||
def block_delimiter
|
||||
@block_delimiter ||= "end#{block_name}"
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def assert_missing_delimitation!
|
||||
raise SyntaxError.new(options[:locale].t("errors.syntax.tag_never_closed".freeze, :block_name => block_name))
|
||||
end
|
||||
def parse_body(body, tokens)
|
||||
body.parse(tokens, parse_context) do |end_tag_name, end_tag_params|
|
||||
@blank &&= body.blank?
|
||||
|
||||
def render_all(list, context)
|
||||
output = []
|
||||
context.resource_limits[:render_length_current] = 0
|
||||
context.resource_limits[:render_score_current] += list.length
|
||||
|
||||
list.each do |token|
|
||||
# Break out if we have any unhanded interrupts.
|
||||
break if context.has_interrupt?
|
||||
|
||||
begin
|
||||
# If we get an Interrupt that means the block must stop processing. An
|
||||
# Interrupt is any command that stops block execution such as {% break %}
|
||||
# or {% continue %}
|
||||
if token.is_a? Continue or token.is_a? Break
|
||||
context.push_interrupt(token.interrupt)
|
||||
break
|
||||
end
|
||||
|
||||
token_output = (token.respond_to?(:render) ? token.render(context) : token)
|
||||
context.increment_used_resources(:render_length_current, token_output)
|
||||
if context.resource_limits_reached?
|
||||
context.resource_limits[:reached] = true
|
||||
raise MemoryError.new("Memory limits exceeded".freeze)
|
||||
end
|
||||
unless token.is_a?(Block) && token.blank?
|
||||
output << token_output
|
||||
end
|
||||
rescue MemoryError => e
|
||||
raise e
|
||||
rescue ::StandardError => e
|
||||
output << (context.handle_error(e))
|
||||
return false if end_tag_name == block_delimiter
|
||||
unless end_tag_name
|
||||
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
|
||||
end
|
||||
|
||||
# this tag is not registered with the system
|
||||
# pass it to the current block for special handling or error reporting
|
||||
unknown_tag(end_tag_name, end_tag_params, tokens)
|
||||
end
|
||||
|
||||
output.join
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
123
lib/liquid/block_body.rb
Normal file
123
lib/liquid/block_body.rb
Normal file
@@ -0,0 +1,123 @@
|
||||
module Liquid
|
||||
class BlockBody
|
||||
FullToken = /\A#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
|
||||
ContentOfVariable = /\A#{VariableStart}(.*)#{VariableEnd}\z/om
|
||||
TAGSTART = "{%".freeze
|
||||
VARSTART = "{{".freeze
|
||||
|
||||
attr_reader :nodelist
|
||||
|
||||
def initialize
|
||||
@nodelist = []
|
||||
@blank = true
|
||||
end
|
||||
|
||||
def parse(tokenizer, parse_context)
|
||||
parse_context.line_number = tokenizer.line_number
|
||||
while token = tokenizer.shift
|
||||
unless token.empty?
|
||||
case
|
||||
when token.start_with?(TAGSTART)
|
||||
if token =~ FullToken
|
||||
tag_name = $1
|
||||
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
|
||||
parse_context.line_number = tokenizer.line_number
|
||||
end
|
||||
|
||||
yield nil, nil
|
||||
end
|
||||
|
||||
def blank?
|
||||
@blank
|
||||
end
|
||||
|
||||
def render(context)
|
||||
output = []
|
||||
context.resource_limits.render_score += @nodelist.length
|
||||
|
||||
@nodelist.each do |token|
|
||||
# Break out if we have any unhanded interrupts.
|
||||
break if context.interrupt?
|
||||
|
||||
begin
|
||||
# If we get an Interrupt that means the block must stop processing. An
|
||||
# Interrupt is any command that stops block execution such as {% break %}
|
||||
# or {% continue %}
|
||||
if token.is_a?(Continue) || token.is_a?(Break)
|
||||
context.push_interrupt(token.interrupt)
|
||||
break
|
||||
end
|
||||
|
||||
node_output = render_node(token, context)
|
||||
|
||||
unless token.is_a?(Block) && token.blank?
|
||||
output << node_output
|
||||
end
|
||||
rescue MemoryError => e
|
||||
raise e
|
||||
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
|
||||
context.handle_error(e, token.line_number)
|
||||
output << nil
|
||||
rescue ::StandardError => e
|
||||
output << context.handle_error(e, token.line_number)
|
||||
end
|
||||
end
|
||||
|
||||
output.join
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def render_node(node, context)
|
||||
node_output = (node.respond_to?(:render) ? node.render(context) : node)
|
||||
node_output = node_output.is_a?(Array) ? node_output.join : node_output.to_s
|
||||
|
||||
context.resource_limits.render_length += node_output.length
|
||||
if context.resource_limits.reached?
|
||||
raise MemoryError.new("Memory limits exceeded".freeze)
|
||||
end
|
||||
node_output
|
||||
end
|
||||
|
||||
def create_variable(token, parse_context)
|
||||
token.scan(ContentOfVariable) do |content|
|
||||
markup = content.first
|
||||
return Variable.new(markup, parse_context)
|
||||
end
|
||||
raise_missing_variable_terminator(token, parse_context)
|
||||
end
|
||||
|
||||
def raise_missing_tag_terminator(token, parse_context)
|
||||
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_termination".freeze, token: token, tag_end: TagEnd.inspect))
|
||||
end
|
||||
|
||||
def raise_missing_variable_terminator(token, parse_context)
|
||||
raise SyntaxError.new(parse_context.locale.t("errors.syntax.variable_termination".freeze, token: token, tag_end: VariableEnd.inspect))
|
||||
end
|
||||
|
||||
def registered_tags
|
||||
Template.tags
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -3,19 +3,26 @@ module Liquid
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# c = Condition.new('1', '==', '1')
|
||||
# c = Condition.new(1, '==', 1)
|
||||
# c.evaluate #=> true
|
||||
#
|
||||
class Condition #:nodoc:
|
||||
@@operators = {
|
||||
'=='.freeze => lambda { |cond, left, right| cond.send(:equal_variables, left, right) },
|
||||
'!='.freeze => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) },
|
||||
'<>'.freeze => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) },
|
||||
'=='.freeze => ->(cond, left, right) { cond.send(:equal_variables, left, right) },
|
||||
'!='.freeze => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
|
||||
'<>'.freeze => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
|
||||
'<'.freeze => :<,
|
||||
'>'.freeze => :>,
|
||||
'>='.freeze => :>=,
|
||||
'<='.freeze => :<=,
|
||||
'contains'.freeze => lambda { |cond, left, right| left && right ? left.include?(right) : false }
|
||||
'contains'.freeze => lambda do |cond, left, right|
|
||||
if left && right && left.respond_to?(:include?)
|
||||
right = right.to_s if left.is_a?(String)
|
||||
left.include?(right)
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
def self.operators
|
||||
@@ -26,7 +33,9 @@ module Liquid
|
||||
attr_accessor :left, :operator, :right
|
||||
|
||||
def initialize(left = nil, operator = nil, right = nil)
|
||||
@left, @operator, @right = left, operator, right
|
||||
@left = left
|
||||
@operator = operator
|
||||
@right = right
|
||||
@child_relation = nil
|
||||
@child_condition = nil
|
||||
end
|
||||
@@ -45,11 +54,13 @@ module Liquid
|
||||
end
|
||||
|
||||
def or(condition)
|
||||
@child_relation, @child_condition = :or, condition
|
||||
@child_relation = :or
|
||||
@child_condition = condition
|
||||
end
|
||||
|
||||
def and(condition)
|
||||
@child_relation, @child_condition = :and, condition
|
||||
@child_relation = :and
|
||||
@child_condition = condition
|
||||
end
|
||||
|
||||
def attach(attachment)
|
||||
@@ -67,17 +78,17 @@ module Liquid
|
||||
private
|
||||
|
||||
def equal_variables(left, right)
|
||||
if left.is_a?(Symbol)
|
||||
if right.respond_to?(left)
|
||||
return right.send(left.to_s)
|
||||
if left.is_a?(Liquid::Expression::MethodLiteral)
|
||||
if right.respond_to?(left.method_name)
|
||||
return right.send(left.method_name)
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
if right.is_a?(Symbol)
|
||||
if left.respond_to?(right)
|
||||
return left.send(right.to_s)
|
||||
if right.is_a?(Liquid::Expression::MethodLiteral)
|
||||
if left.respond_to?(right.method_name)
|
||||
return left.send(right.method_name)
|
||||
else
|
||||
return nil
|
||||
end
|
||||
@@ -90,31 +101,32 @@ module Liquid
|
||||
# If the operator is empty this means that the decision statement is just
|
||||
# a single variable. We can just poll this variable from the context and
|
||||
# return this as the result.
|
||||
return context[left] if op == nil
|
||||
return context.evaluate(left) if op.nil?
|
||||
|
||||
left, right = context[left], context[right]
|
||||
left = context.evaluate(left)
|
||||
right = context.evaluate(right)
|
||||
|
||||
operation = self.class.operators[op] || raise(ArgumentError.new("Unknown operator #{op}"))
|
||||
operation = self.class.operators[op] || raise(Liquid::ArgumentError.new("Unknown operator #{op}"))
|
||||
|
||||
if operation.respond_to?(:call)
|
||||
operation.call(self, left, right)
|
||||
elsif left.respond_to?(operation) and right.respond_to?(operation)
|
||||
left.send(operation, right)
|
||||
else
|
||||
nil
|
||||
elsif left.respond_to?(operation) && right.respond_to?(operation)
|
||||
begin
|
||||
left.send(operation, right)
|
||||
rescue ::ArgumentError => e
|
||||
raise Liquid::ArgumentError.new(e.message)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
class ElseCondition < Condition
|
||||
def else?
|
||||
true
|
||||
end
|
||||
|
||||
def evaluate(context)
|
||||
def evaluate(_context)
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
module Liquid
|
||||
|
||||
# Context keeps the variable stack and resolves variables, as well as keywords
|
||||
#
|
||||
# context['variable'] = 'testing'
|
||||
@@ -14,37 +13,31 @@ module Liquid
|
||||
# context['bob'] #=> nil class Context
|
||||
class Context
|
||||
attr_reader :scopes, :errors, :registers, :environments, :resource_limits
|
||||
attr_accessor :exception_handler, :template_name, :partial, :global_filter, :strict_variables, :strict_filters
|
||||
|
||||
attr_accessor :rethrow_errors
|
||||
|
||||
SQUARE_BRACKETED = /\A\[(.*)\]\z/m
|
||||
|
||||
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = {})
|
||||
@environments = [environments].flatten
|
||||
@scopes = [(outer_scope || {})]
|
||||
@registers = registers
|
||||
@errors = []
|
||||
@rethrow_errors = rethrow_errors
|
||||
@resource_limits = (resource_limits || {}).merge!({ :render_score_current => 0, :assign_score_current => 0 })
|
||||
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil)
|
||||
@environments = [environments].flatten
|
||||
@scopes = [(outer_scope || {})]
|
||||
@registers = registers
|
||||
@errors = []
|
||||
@partial = false
|
||||
@strict_variables = false
|
||||
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
|
||||
squash_instance_assigns_with_environments
|
||||
|
||||
@this_stack_used = false
|
||||
|
||||
if rethrow_errors
|
||||
self.exception_handler = ->(e) { raise }
|
||||
end
|
||||
|
||||
@interrupts = []
|
||||
@filters = []
|
||||
@parsed_variables = Hash.new{ |cache, markup| cache[markup] = variable_parse(markup) }
|
||||
@global_filter = nil
|
||||
end
|
||||
|
||||
def increment_used_resources(key, obj)
|
||||
@resource_limits[key] += if obj.kind_of?(String) || obj.kind_of?(Array) || obj.kind_of?(Hash)
|
||||
obj.length
|
||||
else
|
||||
1
|
||||
end
|
||||
end
|
||||
|
||||
def resource_limits_reached?
|
||||
(@resource_limits[:render_length_limit] && @resource_limits[:render_length_current] > @resource_limits[:render_length_limit]) ||
|
||||
(@resource_limits[:render_score_limit] && @resource_limits[:render_score_current] > @resource_limits[:render_score_limit] ) ||
|
||||
(@resource_limits[:assign_score_limit] && @resource_limits[:assign_score_current] > @resource_limits[:assign_score_limit] )
|
||||
def warnings
|
||||
@warnings ||= []
|
||||
end
|
||||
|
||||
def strainer
|
||||
@@ -57,25 +50,16 @@ module Liquid
|
||||
# for that
|
||||
def add_filters(filters)
|
||||
filters = [filters].flatten.compact
|
||||
filters.each do |f|
|
||||
raise ArgumentError, "Expected module but got: #{f.class}" unless f.is_a?(Module)
|
||||
Strainer.add_known_filter(f)
|
||||
end
|
||||
@filters += filters
|
||||
@strainer = nil
|
||||
end
|
||||
|
||||
# If strainer is already setup then there's no choice but to use a runtime
|
||||
# extend call. If strainer is not yet created, we can utilize strainers
|
||||
# cached class based API, which avoids busting the method cache.
|
||||
if @strainer
|
||||
filters.each do |f|
|
||||
strainer.extend(f)
|
||||
end
|
||||
else
|
||||
@filters.concat filters
|
||||
end
|
||||
def apply_global_filter(obj)
|
||||
global_filter.nil? ? obj : global_filter.call(obj)
|
||||
end
|
||||
|
||||
# are there any not handled interrupts?
|
||||
def has_interrupt?
|
||||
def interrupt?
|
||||
!@interrupts.empty?
|
||||
end
|
||||
|
||||
@@ -89,24 +73,39 @@ module Liquid
|
||||
@interrupts.pop
|
||||
end
|
||||
|
||||
def handle_error(e)
|
||||
errors.push(e)
|
||||
raise if @rethrow_errors
|
||||
|
||||
case e
|
||||
when SyntaxError
|
||||
"Liquid syntax error: #{e.message}"
|
||||
else
|
||||
"Liquid error: #{e.message}"
|
||||
def handle_error(e, line_number = nil)
|
||||
if e.is_a?(Liquid::Error)
|
||||
e.template_name ||= template_name
|
||||
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)
|
||||
output || Liquid::Error.render(e)
|
||||
end
|
||||
|
||||
def invoke(method, *args)
|
||||
strainer.invoke(method, *args)
|
||||
strainer.invoke(method, *args).to_liquid
|
||||
end
|
||||
|
||||
# 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)
|
||||
raise StackLevelError, "Nesting too deep".freeze if @scopes.length > 100
|
||||
end
|
||||
@@ -130,11 +129,19 @@ module Liquid
|
||||
# end
|
||||
#
|
||||
# context['var] #=> nil
|
||||
def stack(new_scope={})
|
||||
push(new_scope)
|
||||
def stack(new_scope = nil)
|
||||
old_stack_used = @this_stack_used
|
||||
if new_scope
|
||||
push(new_scope)
|
||||
@this_stack_used = true
|
||||
else
|
||||
@this_stack_used = false
|
||||
end
|
||||
|
||||
yield
|
||||
ensure
|
||||
pop
|
||||
pop if @this_stack_used
|
||||
@this_stack_used = old_stack_used
|
||||
end
|
||||
|
||||
def clear_instance_assigns
|
||||
@@ -143,162 +150,86 @@ module Liquid
|
||||
|
||||
# Only allow String, Numeric, Hash, Array, Proc, Boolean or <tt>Liquid::Drop</tt>
|
||||
def []=(key, value)
|
||||
unless @this_stack_used
|
||||
@this_stack_used = true
|
||||
push({})
|
||||
end
|
||||
@scopes[0][key] = value
|
||||
end
|
||||
|
||||
def [](key)
|
||||
resolve(key)
|
||||
# Look up variable, either resolve directly after considering the name. We can directly handle
|
||||
# Strings, digits, floats and booleans (true,false).
|
||||
# If no match is made we lookup the variable in the current scope and
|
||||
# later move up to the parent blocks to see if we can resolve the variable somewhere up the tree.
|
||||
# Some special keywords return symbols. Those symbols are to be called on the rhs object in expressions
|
||||
#
|
||||
# Example:
|
||||
# products == empty #=> products.empty?
|
||||
def [](expression)
|
||||
evaluate(Expression.parse(expression))
|
||||
end
|
||||
|
||||
def has_key?(key)
|
||||
resolve(key) != nil
|
||||
def key?(key)
|
||||
self[key] != nil
|
||||
end
|
||||
|
||||
def evaluate(object)
|
||||
object.respond_to?(:evaluate) ? object.evaluate(self) : object
|
||||
end
|
||||
|
||||
# Fetches an object starting at the local scope and then moving up the hierachy
|
||||
def find_variable(key)
|
||||
# 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
|
||||
index = @scopes.find_index { |s| s.key?(key) }
|
||||
scope = @scopes[index] if index
|
||||
|
||||
variable = nil
|
||||
|
||||
if scope.nil?
|
||||
@environments.each do |e|
|
||||
variable = lookup_and_evaluate(e, key)
|
||||
unless variable.nil?
|
||||
scope = e
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
scope ||= @environments.last || @scopes.last
|
||||
variable ||= lookup_and_evaluate(scope, key)
|
||||
|
||||
variable = variable.to_liquid
|
||||
variable.context = self if variable.respond_to?(:context=)
|
||||
|
||||
variable
|
||||
end
|
||||
|
||||
def lookup_and_evaluate(obj, key)
|
||||
if @strict_variables && 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)
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
LITERALS = {
|
||||
nil => nil, 'nil'.freeze => nil, 'null'.freeze => nil, ''.freeze => nil,
|
||||
'true'.freeze => true,
|
||||
'false'.freeze => false,
|
||||
'blank'.freeze => :blank?,
|
||||
'empty'.freeze => :empty?
|
||||
}
|
||||
|
||||
# Look up variable, either resolve directly after considering the name. We can directly handle
|
||||
# Strings, digits, floats and booleans (true,false).
|
||||
# If no match is made we lookup the variable in the current scope and
|
||||
# later move up to the parent blocks to see if we can resolve the variable somewhere up the tree.
|
||||
# Some special keywords return symbols. Those symbols are to be called on the rhs object in expressions
|
||||
#
|
||||
# Example:
|
||||
# products == empty #=> products.empty?
|
||||
def resolve(key)
|
||||
if LITERALS.key?(key)
|
||||
LITERALS[key]
|
||||
else
|
||||
case key
|
||||
when /\A'(.*)'\z/m # Single quoted strings
|
||||
$1
|
||||
when /\A"(.*)"\z/m # Double quoted strings
|
||||
$1
|
||||
when /\A(-?\d+)\z/ # Integer and floats
|
||||
$1.to_i
|
||||
when /\A\((\S+)\.\.(\S+)\)\z/ # Ranges
|
||||
(resolve($1).to_i..resolve($2).to_i)
|
||||
when /\A(-?\d[\d\.]+)\z/ # Floats
|
||||
$1.to_f
|
||||
else
|
||||
variable(key)
|
||||
def squash_instance_assigns_with_environments
|
||||
@scopes.last.each_key do |k|
|
||||
@environments.each do |env|
|
||||
if env.key?(k)
|
||||
scopes.last[k] = lookup_and_evaluate(env, k)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Fetches an object starting at the local scope and then moving up the hierachy
|
||||
def find_variable(key)
|
||||
|
||||
# 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
|
||||
index = @scopes.find_index { |s| s.has_key?(key) }
|
||||
scope = @scopes[index] if index
|
||||
|
||||
variable = nil
|
||||
|
||||
if scope.nil?
|
||||
@environments.each do |e|
|
||||
variable = lookup_and_evaluate(e, key)
|
||||
unless variable.nil?
|
||||
scope = e
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
scope ||= @environments.last || @scopes.last
|
||||
variable ||= lookup_and_evaluate(scope, key)
|
||||
|
||||
variable = variable.to_liquid
|
||||
variable.context = self if variable.respond_to?(:context=)
|
||||
|
||||
return variable
|
||||
end
|
||||
|
||||
def variable_parse(markup)
|
||||
parts = markup.scan(VariableParser)
|
||||
needs_resolution = false
|
||||
if parts.first =~ SQUARE_BRACKETED
|
||||
needs_resolution = true
|
||||
parts[0] = $1
|
||||
end
|
||||
{:first => parts.shift, :needs_resolution => needs_resolution, :rest => parts}
|
||||
end
|
||||
|
||||
# Resolves namespaced queries gracefully.
|
||||
#
|
||||
# Example
|
||||
# @context['hash'] = {"name" => 'tobi'}
|
||||
# assert_equal 'tobi', @context['hash.name']
|
||||
# assert_equal 'tobi', @context['hash["name"]']
|
||||
def variable(markup)
|
||||
parts = @parsed_variables[markup]
|
||||
|
||||
first_part = parts[:first]
|
||||
if parts[:needs_resolution]
|
||||
first_part = resolve(parts[:first])
|
||||
end
|
||||
|
||||
if object = find_variable(first_part)
|
||||
|
||||
parts[:rest].each do |part|
|
||||
part = resolve($1) if part_resolved = (part =~ SQUARE_BRACKETED)
|
||||
|
||||
# If object is a hash- or array-like object we look for the
|
||||
# presence of the key and if its available we return it
|
||||
if object.respond_to?(:[]) and
|
||||
((object.respond_to?(:has_key?) and object.has_key?(part)) or
|
||||
(object.respond_to?(:fetch) and part.is_a?(Integer)))
|
||||
|
||||
# if its a proc we will replace the entry with the proc
|
||||
res = lookup_and_evaluate(object, part)
|
||||
object = res.to_liquid
|
||||
|
||||
# Some special cases. If the part wasn't in square brackets and
|
||||
# no key with the same name was found we interpret following calls
|
||||
# as commands and call them on the current object
|
||||
elsif !part_resolved and object.respond_to?(part) and ['size'.freeze, 'first'.freeze, 'last'.freeze].include?(part)
|
||||
|
||||
object = object.send(part.intern).to_liquid
|
||||
|
||||
# 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
|
||||
else
|
||||
return nil
|
||||
end
|
||||
|
||||
# If we are dealing with a drop here we have to
|
||||
object.context = self if object.respond_to?(:context=)
|
||||
end
|
||||
end
|
||||
|
||||
object
|
||||
end # variable
|
||||
|
||||
def lookup_and_evaluate(obj, key)
|
||||
if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=)
|
||||
obj[key] = (value.arity == 0) ? value.call : value.call(self)
|
||||
else
|
||||
value
|
||||
end
|
||||
end # lookup_and_evaluate
|
||||
|
||||
def squash_instance_assigns_with_environments
|
||||
@scopes.last.each_key do |k|
|
||||
@environments.each do |env|
|
||||
if env.has_key?(k)
|
||||
scopes.last[k] = lookup_and_evaluate(env, k)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end # squash_instance_assigns_with_environments
|
||||
end # squash_instance_assigns_with_environments
|
||||
end # Context
|
||||
|
||||
end # Liquid
|
||||
|
||||
@@ -1,17 +1,27 @@
|
||||
module Liquid
|
||||
class Document < Block
|
||||
def self.parse(tokens, options={})
|
||||
# we don't need markup to open this block
|
||||
super(nil, nil, tokens, options)
|
||||
class Document < BlockBody
|
||||
def self.parse(tokens, parse_context)
|
||||
doc = new
|
||||
doc.parse(tokens, parse_context)
|
||||
doc
|
||||
end
|
||||
|
||||
# There isn't a real delimiter
|
||||
def block_delimiter
|
||||
[]
|
||||
def parse(tokens, parse_context)
|
||||
super do |end_tag_name, end_tag_params|
|
||||
unknown_tag(end_tag_name, parse_context) if end_tag_name
|
||||
end
|
||||
rescue SyntaxError => e
|
||||
e.line_number ||= parse_context.line_number
|
||||
raise
|
||||
end
|
||||
|
||||
# Document blocks don't need to be terminated since they are not actually opened
|
||||
def assert_missing_delimitation!
|
||||
def unknown_tag(tag, parse_context)
|
||||
case tag
|
||||
when 'else'.freeze, 'end'.freeze
|
||||
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_outer_tag".freeze, tag: tag))
|
||||
else
|
||||
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unknown_tag".freeze, tag: tag))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
require 'set'
|
||||
|
||||
module Liquid
|
||||
|
||||
# A drop in liquid is a class which allows you to export DOM like things to liquid.
|
||||
# Methods of drops are callable.
|
||||
# The main use for liquid drops is to implement lazy loaded objects.
|
||||
@@ -19,28 +18,27 @@ module Liquid
|
||||
# tmpl = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {%endfor%} ' )
|
||||
# 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
|
||||
# catch all.
|
||||
# Your drop can either implement the methods sans any parameters
|
||||
# or implement the liquid_method_missing(name) method which is a catch all.
|
||||
class Drop
|
||||
attr_writer :context
|
||||
|
||||
EMPTY_STRING = ''.freeze
|
||||
|
||||
# Catch all for the method
|
||||
def before_method(method)
|
||||
nil
|
||||
def liquid_method_missing(method)
|
||||
return nil unless @context && @context.strict_variables
|
||||
raise Liquid::UndefinedDropMethod, "undefined method #{method}"
|
||||
end
|
||||
|
||||
# called by liquid to invoke a drop
|
||||
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)
|
||||
else
|
||||
before_method(method_or_key)
|
||||
liquid_method_missing(method_or_key)
|
||||
end
|
||||
end
|
||||
|
||||
def has_key?(name)
|
||||
def key?(_name)
|
||||
true
|
||||
end
|
||||
|
||||
@@ -56,22 +54,25 @@ module Liquid
|
||||
self.class.name
|
||||
end
|
||||
|
||||
alias :[] :invoke_drop
|
||||
|
||||
private
|
||||
alias_method :[], :invoke_drop
|
||||
|
||||
# Check for method existence without invoking respond_to?, which creates symbols
|
||||
def self.invokable?(method_name)
|
||||
unless @invokable_methods
|
||||
invokable_methods.include?(method_name.to_s)
|
||||
end
|
||||
|
||||
def self.invokable_methods
|
||||
@invokable_methods ||= begin
|
||||
blacklist = Liquid::Drop.public_instance_methods + [:each]
|
||||
|
||||
if include?(Enumerable)
|
||||
blacklist += Enumerable.public_instance_methods
|
||||
blacklist -= [:sort, :count, :first, :min, :max, :include?]
|
||||
end
|
||||
|
||||
whitelist = [:to_liquid] + (public_instance_methods - blacklist)
|
||||
@invokable_methods = Set.new(whitelist.map(&:to_s))
|
||||
Set.new(whitelist.map(&:to_s))
|
||||
end
|
||||
@invokable_methods.include?(method_name.to_s)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,12 +1,63 @@
|
||||
module Liquid
|
||||
class Error < ::StandardError; end
|
||||
class Error < ::StandardError
|
||||
attr_accessor :line_number
|
||||
attr_accessor :template_name
|
||||
attr_accessor :markup_context
|
||||
|
||||
class ArgumentError < Error; end
|
||||
class ContextError < Error; end
|
||||
class FilterNotFound < Error; end
|
||||
class FileSystemError < Error; end
|
||||
class StandardError < Error; end
|
||||
class SyntaxError < Error; end
|
||||
class StackLevelError < Error; end
|
||||
class MemoryError < Error; end
|
||||
def to_s(with_prefix = true)
|
||||
str = ""
|
||||
str << message_prefix if with_prefix
|
||||
str << super()
|
||||
|
||||
if markup_context
|
||||
str << " "
|
||||
str << markup_context
|
||||
end
|
||||
|
||||
str
|
||||
end
|
||||
|
||||
def self.render(e)
|
||||
if e.is_a?(Liquid::Error)
|
||||
e.to_s
|
||||
else
|
||||
"Liquid error: #{e}"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def message_prefix
|
||||
str = ""
|
||||
if is_a?(SyntaxError)
|
||||
str << "Liquid syntax error"
|
||||
else
|
||||
str << "Liquid error"
|
||||
end
|
||||
|
||||
if line_number
|
||||
str << " ("
|
||||
str << template_name << " " if template_name
|
||||
str << "line " << line_number.to_s << ")"
|
||||
end
|
||||
|
||||
str << ": "
|
||||
str
|
||||
end
|
||||
end
|
||||
|
||||
ArgumentError = Class.new(Error)
|
||||
ContextError = Class.new(Error)
|
||||
FileSystemError = Class.new(Error)
|
||||
StandardError = Class.new(Error)
|
||||
SyntaxError = Class.new(Error)
|
||||
StackLevelError = Class.new(Error)
|
||||
TaintedError = Class.new(Error)
|
||||
MemoryError = Class.new(Error)
|
||||
ZeroDivisionError = Class.new(Error)
|
||||
FloatDomainError = Class.new(Error)
|
||||
UndefinedVariable = Class.new(Error)
|
||||
UndefinedDropMethod = Class.new(Error)
|
||||
UndefinedFilter = Class.new(Error)
|
||||
MethodOverrideError = Class.new(Error)
|
||||
end
|
||||
|
||||
45
lib/liquid/expression.rb
Normal file
45
lib/liquid/expression.rb
Normal file
@@ -0,0 +1,45 @@
|
||||
module Liquid
|
||||
class Expression
|
||||
class MethodLiteral
|
||||
attr_reader :method_name, :to_s
|
||||
|
||||
def initialize(method_name, to_s)
|
||||
@method_name = method_name
|
||||
@to_s = to_s
|
||||
end
|
||||
|
||||
def to_liquid
|
||||
to_s
|
||||
end
|
||||
end
|
||||
|
||||
LITERALS = {
|
||||
nil => nil, 'nil'.freeze => nil, 'null'.freeze => nil, ''.freeze => nil,
|
||||
'true'.freeze => true,
|
||||
'false'.freeze => false,
|
||||
'blank'.freeze => MethodLiteral.new(:blank?, '').freeze,
|
||||
'empty'.freeze => MethodLiteral.new(:empty?, '').freeze
|
||||
}
|
||||
|
||||
def self.parse(markup)
|
||||
if LITERALS.key?(markup)
|
||||
LITERALS[markup]
|
||||
else
|
||||
case markup
|
||||
when /\A'(.*)'\z/m # Single quoted strings
|
||||
$1
|
||||
when /\A"(.*)"\z/m # Double quoted strings
|
||||
$1
|
||||
when /\A(-?\d+)\z/ # Integer and floats
|
||||
$1.to_i
|
||||
when /\A\((\S+)\.\.(\S+)\)\z/ # Ranges
|
||||
RangeLookup.parse($1, $2)
|
||||
when /\A(-?\d[\d\.]+)\z/ # Floats
|
||||
$1.to_f
|
||||
else
|
||||
VariableLookup.parse(markup)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -7,44 +7,44 @@ class String # :nodoc:
|
||||
end
|
||||
end
|
||||
|
||||
class Array # :nodoc:
|
||||
class Array # :nodoc:
|
||||
def to_liquid
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
class Hash # :nodoc:
|
||||
class Hash # :nodoc:
|
||||
def to_liquid
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
class Numeric # :nodoc:
|
||||
class Numeric # :nodoc:
|
||||
def to_liquid
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
class Time # :nodoc:
|
||||
class Time # :nodoc:
|
||||
def to_liquid
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
class DateTime < Date # :nodoc:
|
||||
class DateTime < Date # :nodoc:
|
||||
def to_liquid
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
class Date # :nodoc:
|
||||
class Date # :nodoc:
|
||||
def to_liquid
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
class TrueClass
|
||||
def to_liquid # :nodoc:
|
||||
def to_liquid # :nodoc:
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
@@ -14,7 +14,7 @@ module Liquid
|
||||
# This will parse the template with a LocalFileSystem implementation rooted at 'template_path'.
|
||||
class BlankFileSystem
|
||||
# Called by Liquid to retrieve a template file
|
||||
def read_template_file(template_path, context)
|
||||
def read_template_file(_template_path)
|
||||
raise FileSystemError, "This liquid context does not allow includes."
|
||||
end
|
||||
end
|
||||
@@ -49,9 +49,9 @@ module Liquid
|
||||
@pattern = pattern
|
||||
end
|
||||
|
||||
def read_template_file(template_path, context)
|
||||
def read_template_file(template_path)
|
||||
full_path = full_path(template_path)
|
||||
raise FileSystemError, "No such template '#{template_path}'" unless File.exists?(full_path)
|
||||
raise FileSystemError, "No such template '#{template_path}'" unless File.exist?(full_path)
|
||||
|
||||
File.read(full_path)
|
||||
end
|
||||
@@ -65,7 +65,7 @@ module Liquid
|
||||
File.join(root, @pattern % template_path)
|
||||
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
|
||||
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
|
||||
@@ -2,10 +2,9 @@ require 'yaml'
|
||||
|
||||
module Liquid
|
||||
class I18n
|
||||
DEFAULT_LOCALE = File.join(File.expand_path(File.dirname(__FILE__)), "locales", "en.yml")
|
||||
DEFAULT_LOCALE = File.join(File.expand_path(__dir__), "locales", "en.yml")
|
||||
|
||||
class TranslationError < StandardError
|
||||
end
|
||||
TranslationError = Class.new(StandardError)
|
||||
|
||||
attr_reader :path
|
||||
|
||||
@@ -23,11 +22,12 @@ module Liquid
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def interpolate(name, vars)
|
||||
name.gsub(/%\{(\w+)\}/) {
|
||||
name.gsub(/%\{(\w+)\}/) do
|
||||
# raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym]
|
||||
"#{vars[$1.to_sym]}"
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def deep_fetch_translation(name)
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
module Liquid
|
||||
|
||||
# An interrupt is any command that breaks processing of a block (ex: a for loop).
|
||||
class Interrupt
|
||||
attr_reader :message
|
||||
|
||||
def initialize(message=nil)
|
||||
def initialize(message = nil)
|
||||
@message = message || "interrupt".freeze
|
||||
end
|
||||
end
|
||||
|
||||
@@ -9,9 +9,11 @@ module Liquid
|
||||
'['.freeze => :open_square,
|
||||
']'.freeze => :close_square,
|
||||
'('.freeze => :open_round,
|
||||
')'.freeze => :close_round
|
||||
')'.freeze => :close_round,
|
||||
'?'.freeze => :question,
|
||||
'-'.freeze => :dash
|
||||
}
|
||||
IDENTIFIER = /[\w\-?!]+/
|
||||
IDENTIFIER = /[a-zA-Z_][\w-]*\??/
|
||||
SINGLE_STRING_LITERAL = /'[^\']*'/
|
||||
DOUBLE_STRING_LITERAL = /"[^\"]*"/
|
||||
NUMBER_LITERAL = /-?\d+(\.\d+)?/
|
||||
@@ -25,7 +27,7 @@ module Liquid
|
||||
def tokenize
|
||||
@output = []
|
||||
|
||||
while !@ss.eos?
|
||||
until @ss.eos?
|
||||
@ss.skip(/\s*/)
|
||||
tok = case
|
||||
when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t]
|
||||
@@ -37,7 +39,7 @@ module Liquid
|
||||
else
|
||||
c = @ss.getch
|
||||
if s = SPECIALS[c]
|
||||
[s,c]
|
||||
[s, c]
|
||||
else
|
||||
raise SyntaxError, "Unexpected character #{c}"
|
||||
end
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
errors:
|
||||
syntax:
|
||||
tag_unexpected_args: "Syntax Error in '%{tag}' - Valid syntax: %{tag}"
|
||||
assign: "Syntax Error in 'assign' - Valid syntax: assign [var] = [source]"
|
||||
capture: "Syntax Error in 'capture' - Valid syntax: capture [var]"
|
||||
case: "Syntax Error in 'case' - Valid syntax: case [condition]"
|
||||
@@ -14,7 +15,8 @@
|
||||
include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]"
|
||||
unknown_tag: "Unknown tag '%{tag}'"
|
||||
invalid_delimiter: "'end' 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"
|
||||
tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
|
||||
variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}"
|
||||
tag_never_closed: "'%{block_name}' tag was never closed"
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
# Copyright 2007 by Domizio Demichelis
|
||||
# This library is free software. It may be used, redistributed and/or modified
|
||||
# under the same terms as Ruby itself
|
||||
#
|
||||
# This extension is used in order to expose the object of the implementing class
|
||||
# to liquid as it were a Drop. It also limits the liquid-callable methods of the instance
|
||||
# to the allowed method passed with the liquid_methods call
|
||||
# Example:
|
||||
#
|
||||
# class SomeClass
|
||||
# liquid_methods :an_allowed_method
|
||||
#
|
||||
# def an_allowed_method
|
||||
# 'this comes from an allowed method'
|
||||
# end
|
||||
# def unallowed_method
|
||||
# 'this will never be an output'
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# if you want to extend the drop to other methods you can defines more methods
|
||||
# in the class <YourClass>::LiquidDropClass
|
||||
#
|
||||
# class SomeClass::LiquidDropClass
|
||||
# def another_allowed_method
|
||||
# 'and this from another allowed method'
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# usage:
|
||||
# @something = SomeClass.new
|
||||
#
|
||||
# template:
|
||||
# {{something.an_allowed_method}}{{something.unallowed_method}} {{something.another_allowed_method}}
|
||||
#
|
||||
# output:
|
||||
# 'this comes from an allowed method and this from another allowed method'
|
||||
#
|
||||
# You can also chain associations, by adding the liquid_method call in the
|
||||
# association models.
|
||||
#
|
||||
class Module
|
||||
|
||||
def liquid_methods(*allowed_methods)
|
||||
drop_class = eval "class #{self.to_s}::LiquidDropClass < Liquid::Drop; self; end"
|
||||
define_method :to_liquid do
|
||||
drop_class.new(self)
|
||||
end
|
||||
drop_class.class_eval do
|
||||
def initialize(object)
|
||||
@object = object
|
||||
end
|
||||
allowed_methods.each do |sym|
|
||||
define_method sym do
|
||||
@object.send sym
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
37
lib/liquid/parse_context.rb
Normal file
37
lib/liquid/parse_context.rb
Normal file
@@ -0,0 +1,37 @@
|
||||
module Liquid
|
||||
class ParseContext
|
||||
attr_accessor :locale, :line_number
|
||||
attr_reader :partial, :warnings, :error_mode
|
||||
|
||||
def initialize(options = {})
|
||||
@template_options = options ? options.dup : {}
|
||||
@locale = @template_options[:locale] ||= I18n.new
|
||||
@warnings = []
|
||||
self.partial = false
|
||||
end
|
||||
|
||||
def [](option_key)
|
||||
@options[option_key]
|
||||
end
|
||||
|
||||
def partial=(value)
|
||||
@partial = value
|
||||
@options = value ? partial_options : @template_options
|
||||
@error_mode = @options[:error_mode] || Template.error_mode
|
||||
value
|
||||
end
|
||||
|
||||
def partial_options
|
||||
@partial_options ||= begin
|
||||
dont_pass = @template_options[:include_options_blacklist]
|
||||
if dont_pass == true
|
||||
{ locale: locale }
|
||||
elsif dont_pass.is_a?(Array)
|
||||
@template_options.reject { |k, v| dont_pass.include?(k) }
|
||||
else
|
||||
@template_options
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -75,7 +75,7 @@ module Liquid
|
||||
|
||||
def variable_signature
|
||||
str = consume(:id)
|
||||
if look(:open_square)
|
||||
while look(:open_square)
|
||||
str << consume
|
||||
str << expression
|
||||
str << consume(:close_square)
|
||||
|
||||
31
lib/liquid/parser_switching.rb
Normal file
31
lib/liquid/parser_switching.rb
Normal file
@@ -0,0 +1,31 @@
|
||||
module Liquid
|
||||
module ParserSwitching
|
||||
def parse_with_selected_parser(markup)
|
||||
case parse_context.error_mode
|
||||
when :strict then strict_parse_with_error_context(markup)
|
||||
when :lax then lax_parse(markup)
|
||||
when :warn
|
||||
begin
|
||||
return strict_parse_with_error_context(markup)
|
||||
rescue SyntaxError => e
|
||||
parse_context.warnings << e
|
||||
return lax_parse(markup)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def strict_parse_with_error_context(markup)
|
||||
strict_parse(markup)
|
||||
rescue SyntaxError => e
|
||||
e.line_number = line_number
|
||||
e.markup_context = markup_context(markup)
|
||||
raise e
|
||||
end
|
||||
|
||||
def markup_context(markup)
|
||||
"in \"#{markup.strip}\""
|
||||
end
|
||||
end
|
||||
end
|
||||
158
lib/liquid/profiler.rb
Normal file
158
lib/liquid/profiler.rb
Normal file
@@ -0,0 +1,158 @@
|
||||
require 'liquid/profiler/hooks'
|
||||
|
||||
module Liquid
|
||||
# Profiler enables support for profiling template rendering to help track down performance issues.
|
||||
#
|
||||
# To enable profiling, first require 'liquid/profiler'.
|
||||
# Then, to profile a parse/render cycle, pass the <tt>profile: true</tt> option to <tt>Liquid::Template.parse</tt>.
|
||||
# After <tt>Liquid::Template#render</tt> is called, the template object makes available an instance of this
|
||||
# class via the <tt>Liquid::Template#profiler</tt> method.
|
||||
#
|
||||
# template = Liquid::Template.parse(template_content, profile: true)
|
||||
# output = template.render
|
||||
# profile = template.profiler
|
||||
#
|
||||
# This object contains all profiling information, containing information on what tags were rendered,
|
||||
# where in the templates these tags live, and how long each tag took to render.
|
||||
#
|
||||
# This is a tree structure that is Enumerable all the way down, and keeps track of tags and rendering times
|
||||
# inside of <tt>{% include %}</tt> tags.
|
||||
#
|
||||
# profile.each do |node|
|
||||
# # Access to the node itself
|
||||
# node.code
|
||||
#
|
||||
# # Which template and line number of this node.
|
||||
# # If top level, this will be "<root>".
|
||||
# node.partial
|
||||
# node.line_number
|
||||
#
|
||||
# # Render time in seconds of this node
|
||||
# node.render_time
|
||||
#
|
||||
# # If the template used {% include %}, this node will also have children.
|
||||
# node.children.each do |child2|
|
||||
# # ...
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Profiler also exposes the total time of the template's render in <tt>Liquid::Profiler#total_render_time</tt>.
|
||||
#
|
||||
# All render times are in seconds. There is a small performance hit when profiling is enabled.
|
||||
#
|
||||
class Profiler
|
||||
include Enumerable
|
||||
|
||||
class Timing
|
||||
attr_reader :code, :partial, :line_number, :children
|
||||
|
||||
def initialize(node, partial)
|
||||
@code = node.respond_to?(:raw) ? node.raw : node
|
||||
@partial = partial
|
||||
@line_number = node.respond_to?(:line_number) ? node.line_number : nil
|
||||
@children = []
|
||||
end
|
||||
|
||||
def self.start(node, partial)
|
||||
new(node, partial).tap(&:start)
|
||||
end
|
||||
|
||||
def start
|
||||
@start_time = Time.now
|
||||
end
|
||||
|
||||
def finish
|
||||
@end_time = Time.now
|
||||
end
|
||||
|
||||
def render_time
|
||||
@end_time - @start_time
|
||||
end
|
||||
end
|
||||
|
||||
def self.profile_node_render(node)
|
||||
if Profiler.current_profile && node.respond_to?(:render)
|
||||
Profiler.current_profile.start_node(node)
|
||||
output = yield
|
||||
Profiler.current_profile.end_node(node)
|
||||
output
|
||||
else
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
def self.profile_children(template_name)
|
||||
if Profiler.current_profile
|
||||
Profiler.current_profile.push_partial(template_name)
|
||||
output = yield
|
||||
Profiler.current_profile.pop_partial
|
||||
output
|
||||
else
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
def self.current_profile
|
||||
Thread.current[:liquid_profiler]
|
||||
end
|
||||
|
||||
def initialize
|
||||
@partial_stack = ["<root>"]
|
||||
|
||||
@root_timing = Timing.new("", current_partial)
|
||||
@timing_stack = [@root_timing]
|
||||
|
||||
@render_start_at = Time.now
|
||||
@render_end_at = @render_start_at
|
||||
end
|
||||
|
||||
def start
|
||||
Thread.current[:liquid_profiler] = self
|
||||
@render_start_at = Time.now
|
||||
end
|
||||
|
||||
def stop
|
||||
Thread.current[:liquid_profiler] = nil
|
||||
@render_end_at = Time.now
|
||||
end
|
||||
|
||||
def total_render_time
|
||||
@render_end_at - @render_start_at
|
||||
end
|
||||
|
||||
def each(&block)
|
||||
@root_timing.children.each(&block)
|
||||
end
|
||||
|
||||
def [](idx)
|
||||
@root_timing.children[idx]
|
||||
end
|
||||
|
||||
def length
|
||||
@root_timing.children.length
|
||||
end
|
||||
|
||||
def start_node(node)
|
||||
@timing_stack.push(Timing.start(node, current_partial))
|
||||
end
|
||||
|
||||
def end_node(_node)
|
||||
timing = @timing_stack.pop
|
||||
timing.finish
|
||||
|
||||
@timing_stack.last.children << timing
|
||||
end
|
||||
|
||||
def current_partial
|
||||
@partial_stack.last
|
||||
end
|
||||
|
||||
def push_partial(partial_name)
|
||||
@partial_stack.push(partial_name)
|
||||
end
|
||||
|
||||
def pop_partial
|
||||
@partial_stack.pop
|
||||
end
|
||||
end
|
||||
end
|
||||
23
lib/liquid/profiler/hooks.rb
Normal file
23
lib/liquid/profiler/hooks.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
module Liquid
|
||||
class BlockBody
|
||||
def render_node_with_profiling(node, context)
|
||||
Profiler.profile_node_render(node) do
|
||||
render_node_without_profiling(node, context)
|
||||
end
|
||||
end
|
||||
|
||||
alias_method :render_node_without_profiling, :render_node
|
||||
alias_method :render_node, :render_node_with_profiling
|
||||
end
|
||||
|
||||
class Include < Tag
|
||||
def render_with_profiling(context)
|
||||
Profiler.profile_children(context.evaluate(@template_name_expr).to_s) do
|
||||
render_without_profiling(context)
|
||||
end
|
||||
end
|
||||
|
||||
alias_method :render_without_profiling, :render
|
||||
alias_method :render, :render_with_profiling
|
||||
end
|
||||
end
|
||||
37
lib/liquid/range_lookup.rb
Normal file
37
lib/liquid/range_lookup.rb
Normal file
@@ -0,0 +1,37 @@
|
||||
module Liquid
|
||||
class RangeLookup
|
||||
def self.parse(start_markup, end_markup)
|
||||
start_obj = Expression.parse(start_markup)
|
||||
end_obj = Expression.parse(end_markup)
|
||||
if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate)
|
||||
new(start_obj, end_obj)
|
||||
else
|
||||
start_obj.to_i..end_obj.to_i
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(start_obj, end_obj)
|
||||
@start_obj = start_obj
|
||||
@end_obj = end_obj
|
||||
end
|
||||
|
||||
def evaluate(context)
|
||||
start_int = to_integer(context.evaluate(@start_obj))
|
||||
end_int = to_integer(context.evaluate(@end_obj))
|
||||
start_int..end_int
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def to_integer(input)
|
||||
case input
|
||||
when Integer
|
||||
input
|
||||
when NilClass, String
|
||||
input.to_i
|
||||
else
|
||||
Utils.to_integer(input)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
23
lib/liquid/resource_limits.rb
Normal file
23
lib/liquid/resource_limits.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
module Liquid
|
||||
class ResourceLimits
|
||||
attr_accessor :render_length, :render_score, :assign_score,
|
||||
:render_length_limit, :render_score_limit, :assign_score_limit
|
||||
|
||||
def initialize(limits)
|
||||
@render_length_limit = limits[:render_length_limit]
|
||||
@render_score_limit = limits[:render_score_limit]
|
||||
@assign_score_limit = limits[:assign_score_limit]
|
||||
reset
|
||||
end
|
||||
|
||||
def reached?
|
||||
(@render_length_limit && @render_length > @render_length_limit) ||
|
||||
(@render_score_limit && @render_score > @render_score_limit) ||
|
||||
(@assign_score_limit && @assign_score > @assign_score_limit)
|
||||
end
|
||||
|
||||
def reset
|
||||
@render_length = @render_score = @assign_score = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -2,7 +2,6 @@ require 'cgi'
|
||||
require 'bigdecimal'
|
||||
|
||||
module Liquid
|
||||
|
||||
module StandardFilters
|
||||
HTML_ESCAPE = {
|
||||
'&'.freeze => '&'.freeze,
|
||||
@@ -34,27 +33,48 @@ module Liquid
|
||||
end
|
||||
|
||||
def escape(input)
|
||||
CGI.escapeHTML(input) rescue input
|
||||
CGI.escapeHTML(input).untaint unless input.nil?
|
||||
end
|
||||
alias_method :h, :escape
|
||||
|
||||
def escape_once(input)
|
||||
input.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE)
|
||||
end
|
||||
|
||||
alias_method :h, :escape
|
||||
def url_encode(input)
|
||||
CGI.escape(input) unless input.nil?
|
||||
end
|
||||
|
||||
def url_decode(input)
|
||||
CGI.unescape(input) unless input.nil?
|
||||
end
|
||||
|
||||
def slice(input, offset, length = nil)
|
||||
offset = Utils.to_integer(offset)
|
||||
length = length ? Utils.to_integer(length) : 1
|
||||
|
||||
if input.is_a?(Array)
|
||||
input.slice(offset, length) || []
|
||||
else
|
||||
input.to_s.slice(offset, length) || ''
|
||||
end
|
||||
end
|
||||
|
||||
# Truncate a string down to x characters
|
||||
def truncate(input, length = 50, truncate_string = "...".freeze)
|
||||
if input.nil? then return end
|
||||
l = length.to_i - truncate_string.length
|
||||
return if input.nil?
|
||||
input_str = input.to_s
|
||||
length = Utils.to_integer(length)
|
||||
l = length - truncate_string.length
|
||||
l = 0 if l < 0
|
||||
input.length > length.to_i ? input[0...l] + truncate_string : input
|
||||
input_str.length > length ? input_str[0...l] + truncate_string : input_str
|
||||
end
|
||||
|
||||
def truncatewords(input, words = 15, truncate_string = "...".freeze)
|
||||
if input.nil? then return end
|
||||
return if input.nil?
|
||||
wordlist = input.to_s.split
|
||||
l = words.to_i - 1
|
||||
words = Utils.to_integer(words)
|
||||
l = words - 1
|
||||
l = 0 if l < 0
|
||||
wordlist.length > l ? wordlist[0..l].join(" ".freeze) + truncate_string : input
|
||||
end
|
||||
@@ -65,7 +85,7 @@ module Liquid
|
||||
# <div class="summary">{{ post | split '//' | first }}</div>
|
||||
#
|
||||
def split(input, pattern)
|
||||
input.split(pattern)
|
||||
input.to_s.split(pattern)
|
||||
end
|
||||
|
||||
def strip(input)
|
||||
@@ -92,59 +112,102 @@ module Liquid
|
||||
|
||||
# Join elements of the array with certain character between them
|
||||
def join(input, glue = ' '.freeze)
|
||||
[input].flatten.join(glue)
|
||||
InputIterator.new(input).join(glue)
|
||||
end
|
||||
|
||||
# Sort elements of the array
|
||||
# provide optional property with which to sort an array of hashes or drops
|
||||
def sort(input, property = nil)
|
||||
ary = flatten_if_necessary(input)
|
||||
ary = InputIterator.new(input)
|
||||
if property.nil?
|
||||
ary.sort
|
||||
elsif ary.first.respond_to?('[]'.freeze) and !ary.first[property].nil?
|
||||
ary.sort {|a,b| a[property] <=> b[property] }
|
||||
elsif ary.first.respond_to?(property)
|
||||
ary.sort {|a,b| a.send(property) <=> b.send(property) }
|
||||
elsif ary.empty? # The next two cases assume a non-empty array.
|
||||
[]
|
||||
elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
|
||||
ary.sort { |a, b| a[property] <=> b[property] }
|
||||
end
|
||||
end
|
||||
|
||||
# Sort elements of an array ignoring case if strings
|
||||
# provide optional property with which to sort an array of hashes or drops
|
||||
def sort_natural(input, property = nil)
|
||||
ary = InputIterator.new(input)
|
||||
|
||||
if property.nil?
|
||||
ary.sort { |a, b| a.casecmp(b) }
|
||||
elsif ary.empty? # The next two cases assume a non-empty array.
|
||||
[]
|
||||
elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
|
||||
ary.sort { |a, b| a[property].casecmp(b[property]) }
|
||||
end
|
||||
end
|
||||
|
||||
# Remove duplicate elements from an array
|
||||
# provide optional property with which to determine uniqueness
|
||||
def uniq(input, property = nil)
|
||||
ary = InputIterator.new(input)
|
||||
|
||||
if property.nil?
|
||||
ary.uniq
|
||||
elsif ary.empty? # The next two cases assume a non-empty array.
|
||||
[]
|
||||
elsif ary.first.respond_to?(:[])
|
||||
ary.uniq{ |a| a[property] }
|
||||
end
|
||||
end
|
||||
|
||||
# Reverse the elements of an array
|
||||
def reverse(input)
|
||||
ary = [input].flatten
|
||||
ary = InputIterator.new(input)
|
||||
ary.reverse
|
||||
end
|
||||
|
||||
# map/collect on a given property
|
||||
def map(input, property)
|
||||
flatten_if_necessary(input).map do |e|
|
||||
InputIterator.new(input).map do |e|
|
||||
e = e.call if e.is_a?(Proc)
|
||||
|
||||
if property == "to_liquid".freeze
|
||||
e
|
||||
elsif e.respond_to?(:[])
|
||||
e[property]
|
||||
r = e[property]
|
||||
r.is_a?(Proc) ? r.call : r
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Remove nils within an array
|
||||
# provide optional property with which to check for nil
|
||||
def compact(input, property = nil)
|
||||
ary = InputIterator.new(input)
|
||||
|
||||
if property.nil?
|
||||
ary.compact
|
||||
elsif ary.empty? # The next two cases assume a non-empty array.
|
||||
[]
|
||||
elsif ary.first.respond_to?(:[])
|
||||
ary.reject{ |a| a[property].nil? }
|
||||
end
|
||||
end
|
||||
|
||||
# Replace occurrences of a string with another
|
||||
def replace(input, string, replacement = ''.freeze)
|
||||
input.to_s.gsub(string, replacement.to_s)
|
||||
input.to_s.gsub(string.to_s, replacement.to_s)
|
||||
end
|
||||
|
||||
# Replace the first occurrences of a string with another
|
||||
def replace_first(input, string, replacement = ''.freeze)
|
||||
input.to_s.sub(string, replacement.to_s)
|
||||
input.to_s.sub(string.to_s, replacement.to_s)
|
||||
end
|
||||
|
||||
# remove a substring
|
||||
def remove(input, string)
|
||||
input.to_s.gsub(string, ''.freeze)
|
||||
input.to_s.gsub(string.to_s, ''.freeze)
|
||||
end
|
||||
|
||||
# remove the first occurrences of a substring
|
||||
def remove_first(input, string)
|
||||
input.to_s.sub(string, ''.freeze)
|
||||
input.to_s.sub(string.to_s, ''.freeze)
|
||||
end
|
||||
|
||||
# add one string to another
|
||||
@@ -152,6 +215,13 @@ module Liquid
|
||||
input.to_s + string.to_s
|
||||
end
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
# prepend a string to another
|
||||
def prepend(input, string)
|
||||
string.to_s + input.to_s
|
||||
@@ -196,7 +266,7 @@ module Liquid
|
||||
def date(input, format)
|
||||
return input if format.to_s.empty?
|
||||
|
||||
return input unless date = to_date(input)
|
||||
return input unless date = Utils.to_date(input)
|
||||
|
||||
date.strftime(format.to_s)
|
||||
end
|
||||
@@ -237,79 +307,98 @@ module Liquid
|
||||
# division
|
||||
def divided_by(input, operand)
|
||||
apply_operation(input, operand, :/)
|
||||
rescue ::ZeroDivisionError => e
|
||||
raise Liquid::ZeroDivisionError, e.message
|
||||
end
|
||||
|
||||
def modulo(input, operand)
|
||||
apply_operation(input, operand, :%)
|
||||
rescue ::ZeroDivisionError => e
|
||||
raise Liquid::ZeroDivisionError, e.message
|
||||
end
|
||||
|
||||
def round(input, n = 0)
|
||||
result = to_number(input).round(to_number(n))
|
||||
result = Utils.to_number(input).round(Utils.to_number(n))
|
||||
result = result.to_f if result.is_a?(BigDecimal)
|
||||
result = result.to_i if n == 0
|
||||
result
|
||||
rescue ::FloatDomainError => e
|
||||
raise Liquid::FloatDomainError, e.message
|
||||
end
|
||||
|
||||
def ceil(input)
|
||||
to_number(input).ceil.to_i
|
||||
Utils.to_number(input).ceil.to_i
|
||||
rescue ::FloatDomainError => e
|
||||
raise Liquid::FloatDomainError, e.message
|
||||
end
|
||||
|
||||
def floor(input)
|
||||
to_number(input).floor.to_i
|
||||
Utils.to_number(input).floor.to_i
|
||||
rescue ::FloatDomainError => e
|
||||
raise Liquid::FloatDomainError, e.message
|
||||
end
|
||||
|
||||
def default(input, default_value = "".freeze)
|
||||
is_blank = input.respond_to?(:empty?) ? input.empty? : !input
|
||||
is_blank ? default_value : input
|
||||
def default(input, default_value = ''.freeze)
|
||||
if !input || input.respond_to?(:empty?) && input.empty?
|
||||
default_value
|
||||
else
|
||||
input
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def flatten_if_necessary(input)
|
||||
ary = if input.is_a?(Array)
|
||||
input.flatten
|
||||
elsif input.is_a?(Enumerable) && !input.is_a?(Hash)
|
||||
input
|
||||
else
|
||||
[input].flatten
|
||||
end
|
||||
ary.map{ |e| e.respond_to?(:to_liquid) ? e.to_liquid : e }
|
||||
end
|
||||
|
||||
def to_number(obj)
|
||||
case obj
|
||||
when Float
|
||||
BigDecimal.new(obj.to_s)
|
||||
when Numeric
|
||||
obj
|
||||
when String
|
||||
(obj.strip =~ /\A\d+\.\d+\z/) ? BigDecimal.new(obj) : obj.to_i
|
||||
else
|
||||
0
|
||||
end
|
||||
end
|
||||
|
||||
def to_date(obj)
|
||||
return obj if obj.respond_to?(:strftime)
|
||||
|
||||
case obj
|
||||
when 'now'.freeze, 'today'.freeze
|
||||
Time.now
|
||||
when /\A\d+\z/, Integer
|
||||
Time.at(obj.to_i)
|
||||
when String
|
||||
Time.parse(obj)
|
||||
else
|
||||
nil
|
||||
end
|
||||
rescue ArgumentError
|
||||
nil
|
||||
end
|
||||
|
||||
def apply_operation(input, operand, operation)
|
||||
result = to_number(input).send(operation, to_number(operand))
|
||||
result = Utils.to_number(input).send(operation, Utils.to_number(operand))
|
||||
result.is_a?(BigDecimal) ? result.to_f : result
|
||||
end
|
||||
|
||||
class InputIterator
|
||||
include Enumerable
|
||||
|
||||
def initialize(input)
|
||||
@input = if input.is_a?(Array)
|
||||
input.flatten
|
||||
elsif input.is_a?(Hash)
|
||||
[input]
|
||||
elsif input.is_a?(Enumerable)
|
||||
input
|
||||
else
|
||||
Array(input)
|
||||
end
|
||||
end
|
||||
|
||||
def join(glue)
|
||||
to_a.join(glue)
|
||||
end
|
||||
|
||||
def concat(args)
|
||||
to_a.concat(args)
|
||||
end
|
||||
|
||||
def reverse
|
||||
reverse_each.to_a
|
||||
end
|
||||
|
||||
def uniq(&block)
|
||||
to_a.uniq(&block)
|
||||
end
|
||||
|
||||
def compact
|
||||
to_a.compact
|
||||
end
|
||||
|
||||
def empty?
|
||||
@input.each { return false }
|
||||
true
|
||||
end
|
||||
|
||||
def each
|
||||
@input.each do |e|
|
||||
yield(e.respond_to?(:to_liquid) ? e.to_liquid : e)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_filter(StandardFilters)
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
require 'set'
|
||||
|
||||
module Liquid
|
||||
|
||||
# Strainer is the parent class for the filters system.
|
||||
# New filters are mixed into the strainer class which is then instantiated for each liquid template render run.
|
||||
#
|
||||
# The Strainer only allows method calls defined in filters given to it via Strainer.global_filter,
|
||||
# Context#add_filters or Template.register_filter
|
||||
class Strainer #:nodoc:
|
||||
@@filters = []
|
||||
@@known_filters = Set.new
|
||||
@@known_methods = Set.new
|
||||
@@global_strainer = Class.new(Strainer) do
|
||||
@filter_methods = Set.new
|
||||
end
|
||||
@@strainer_class_cache = Hash.new do |hash, filters|
|
||||
hash[filters] = Class.new(Strainer) do
|
||||
filters.each { |f| include f }
|
||||
hash[filters] = Class.new(@@global_strainer) do
|
||||
@filter_methods = @@global_strainer.filter_methods.dup
|
||||
filters.each { |f| add_filter(f) }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -21,43 +21,45 @@ module Liquid
|
||||
@context = context
|
||||
end
|
||||
|
||||
def self.global_filter(filter)
|
||||
raise ArgumentError, "Passed filter is not a module" unless filter.is_a?(Module)
|
||||
add_known_filter(filter)
|
||||
@@filters << filter unless @@filters.include?(filter)
|
||||
class << self
|
||||
attr_reader :filter_methods
|
||||
end
|
||||
|
||||
def self.add_known_filter(filter)
|
||||
unless @@known_filters.include?(filter)
|
||||
@@method_blacklist ||= Set.new(Strainer.instance_methods.map(&:to_s))
|
||||
new_methods = filter.instance_methods.map(&:to_s)
|
||||
new_methods.reject!{ |m| @@method_blacklist.include?(m) }
|
||||
@@known_methods.merge(new_methods)
|
||||
@@known_filters.add(filter)
|
||||
def self.add_filter(filter)
|
||||
raise ArgumentError, "Expected module but got: #{filter.class}" unless filter.is_a?(Module)
|
||||
unless self.class.include?(filter)
|
||||
invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }
|
||||
if invokable_non_public_methods.any?
|
||||
raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"
|
||||
else
|
||||
send(:include, filter)
|
||||
@filter_methods.merge(filter.public_instance_methods.map(&:to_s))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.strainer_class_cache
|
||||
@@strainer_class_cache
|
||||
def self.global_filter(filter)
|
||||
@@global_strainer.add_filter(filter)
|
||||
end
|
||||
|
||||
def self.invokable?(method)
|
||||
@filter_methods.include?(method.to_s)
|
||||
end
|
||||
|
||||
def self.create(context, filters = [])
|
||||
filters = @@filters + filters
|
||||
strainer_class_cache[filters].new(context)
|
||||
@@strainer_class_cache[filters].new(context)
|
||||
end
|
||||
|
||||
def invoke(method, *args)
|
||||
if invokable?(method)
|
||||
if self.class.invokable?(method)
|
||||
send(method, *args)
|
||||
elsif @context && @context.strict_filters
|
||||
raise Liquid::UndefinedFilter, "undefined filter #{method}"
|
||||
else
|
||||
args.first
|
||||
end
|
||||
rescue ::ArgumentError => e
|
||||
raise Liquid::ArgumentError.new(e.message)
|
||||
end
|
||||
|
||||
def invokable?(method)
|
||||
@@known_methods.include?(method.to_s) && respond_to?(method)
|
||||
raise Liquid::ArgumentError, e.message, e.backtrace
|
||||
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
|
||||
@@ -1,61 +1,43 @@
|
||||
module Liquid
|
||||
class Tag
|
||||
attr_accessor :options
|
||||
attr_reader :nodelist, :warnings
|
||||
attr_reader :nodelist, :tag_name, :line_number, :parse_context
|
||||
alias_method :options, :parse_context
|
||||
include ParserSwitching
|
||||
|
||||
class << self
|
||||
def parse(tag_name, markup, tokens, options)
|
||||
def parse(tag_name, markup, tokenizer, options)
|
||||
tag = new(tag_name, markup, options)
|
||||
tag.parse(tokens)
|
||||
tag.parse(tokenizer)
|
||||
tag
|
||||
end
|
||||
|
||||
private :new
|
||||
end
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
def initialize(tag_name, markup, parse_context)
|
||||
@tag_name = tag_name
|
||||
@markup = markup
|
||||
@options = options
|
||||
@parse_context = parse_context
|
||||
@line_number = parse_context.line_number
|
||||
end
|
||||
|
||||
def parse(tokens)
|
||||
def parse(_tokens)
|
||||
end
|
||||
|
||||
def raw
|
||||
"#{@tag_name} #{@markup}"
|
||||
end
|
||||
|
||||
def name
|
||||
self.class.name.downcase
|
||||
end
|
||||
|
||||
def render(context)
|
||||
def render(_context)
|
||||
''.freeze
|
||||
end
|
||||
|
||||
def blank?
|
||||
false
|
||||
end
|
||||
|
||||
def parse_with_selected_parser(markup)
|
||||
case @options[:error_mode] || Template.error_mode
|
||||
when :strict then strict_parse_with_error_context(markup)
|
||||
when :lax then lax_parse(markup)
|
||||
when :warn
|
||||
begin
|
||||
return strict_parse_with_error_context(markup)
|
||||
rescue SyntaxError => e
|
||||
@warnings ||= []
|
||||
@warnings << e
|
||||
return lax_parse(markup)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def strict_parse_with_error_context(markup)
|
||||
strict_parse(markup)
|
||||
rescue SyntaxError => e
|
||||
e.message << " in \"#{markup.strip}\""
|
||||
raise e
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
module Liquid
|
||||
|
||||
# Assign sets a variable in your template.
|
||||
#
|
||||
# {% assign foo = 'monkey' %}
|
||||
@@ -15,7 +14,7 @@ module Liquid
|
||||
super
|
||||
if markup =~ Syntax
|
||||
@to = $1
|
||||
@from = Variable.new($2)
|
||||
@from = Variable.new($2, options)
|
||||
else
|
||||
raise SyntaxError.new options[:locale].t("errors.syntax.assign".freeze)
|
||||
end
|
||||
@@ -24,13 +23,28 @@ module Liquid
|
||||
def render(context)
|
||||
val = @from.render(context)
|
||||
context.scopes.last[@to] = val
|
||||
context.increment_used_resources(:assign_score_current, val)
|
||||
context.resource_limits.assign_score += assign_score_of(val)
|
||||
''.freeze
|
||||
end
|
||||
|
||||
def blank?
|
||||
true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def assign_score_of(val)
|
||||
if val.instance_of?(String)
|
||||
val.length
|
||||
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
|
||||
end
|
||||
|
||||
Template.register_tag('assign'.freeze, Assign)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
module Liquid
|
||||
|
||||
# Break tag to be used to break out of a for loop.
|
||||
#
|
||||
# == Basic Usage:
|
||||
@@ -10,11 +9,9 @@ module Liquid
|
||||
# {% endfor %}
|
||||
#
|
||||
class Break < Tag
|
||||
|
||||
def interrupt
|
||||
BreakInterrupt.new
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
Template.register_tag('break'.freeze, Break)
|
||||
|
||||
@@ -11,7 +11,7 @@ module Liquid
|
||||
# in a sidebar or footer.
|
||||
#
|
||||
class Capture < Block
|
||||
Syntax = /(\w+)/
|
||||
Syntax = /(#{VariableSignature}+)/o
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
@@ -25,7 +25,7 @@ module Liquid
|
||||
def render(context)
|
||||
output = super
|
||||
context.scopes.last[@to] = output
|
||||
context.increment_used_resources(:assign_score_current, output)
|
||||
context.resource_limits.assign_score += output.length
|
||||
''.freeze
|
||||
end
|
||||
|
||||
|
||||
@@ -8,18 +8,24 @@ module Liquid
|
||||
@blocks = []
|
||||
|
||||
if markup =~ Syntax
|
||||
@left = $1
|
||||
@left = Expression.parse($1)
|
||||
else
|
||||
raise SyntaxError.new(options[:locale].t("errors.syntax.case".freeze))
|
||||
end
|
||||
end
|
||||
|
||||
def parse(tokens)
|
||||
body = BlockBody.new
|
||||
while parse_body(body, tokens)
|
||||
body = @blocks.last.attachment
|
||||
end
|
||||
end
|
||||
|
||||
def nodelist
|
||||
@blocks.map(&:attachment).flatten
|
||||
@blocks.map(&:attachment)
|
||||
end
|
||||
|
||||
def unknown_tag(tag, markup, tokens)
|
||||
@nodelist = []
|
||||
case tag
|
||||
when 'when'.freeze
|
||||
record_when_condition(markup)
|
||||
@@ -37,10 +43,10 @@ module Liquid
|
||||
output = ''
|
||||
@blocks.each do |block|
|
||||
if block.else?
|
||||
return render_all(block.attachment, context) if execute_else_block
|
||||
return block.attachment.render(context) if execute_else_block
|
||||
elsif block.evaluate(context)
|
||||
execute_else_block = false
|
||||
output << render_all(block.attachment, context)
|
||||
output << block.attachment.render(context)
|
||||
end
|
||||
end
|
||||
output
|
||||
@@ -50,27 +56,28 @@ module Liquid
|
||||
private
|
||||
|
||||
def record_when_condition(markup)
|
||||
body = BlockBody.new
|
||||
|
||||
while markup
|
||||
# Create a new nodelist and assign it to the new block
|
||||
if not markup =~ WhenSyntax
|
||||
unless markup =~ WhenSyntax
|
||||
raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_when".freeze))
|
||||
end
|
||||
|
||||
markup = $2
|
||||
|
||||
block = Condition.new(@left, '=='.freeze, $1)
|
||||
block.attach(@nodelist)
|
||||
@blocks.push(block)
|
||||
block = Condition.new(@left, '=='.freeze, Expression.parse($1))
|
||||
block.attach(body)
|
||||
@blocks << block
|
||||
end
|
||||
end
|
||||
|
||||
def record_else_condition(markup)
|
||||
if not markup.strip.empty?
|
||||
unless markup.strip.empty?
|
||||
raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_else".freeze))
|
||||
end
|
||||
|
||||
block = ElseCondition.new
|
||||
block.attach(@nodelist)
|
||||
block.attach(BlockBody.new)
|
||||
@blocks << block
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
module Liquid
|
||||
class Comment < Block
|
||||
def render(context)
|
||||
def render(_context)
|
||||
''.freeze
|
||||
end
|
||||
|
||||
def unknown_tag(tag, markup, tokens)
|
||||
def unknown_tag(_tag, _markup, _tokens)
|
||||
end
|
||||
|
||||
def blank?
|
||||
|
||||
@@ -20,10 +20,10 @@ module Liquid
|
||||
case markup
|
||||
when NamedSyntax
|
||||
@variables = variables_from_string($2)
|
||||
@name = $1
|
||||
@name = Expression.parse($1)
|
||||
when SimpleSyntax
|
||||
@variables = variables_from_string(markup)
|
||||
@name = "'#{@variables.to_s}'"
|
||||
@name = @variables.to_s
|
||||
else
|
||||
raise SyntaxError.new(options[:locale].t("errors.syntax.cycle".freeze))
|
||||
end
|
||||
@@ -33,11 +33,11 @@ module Liquid
|
||||
context.registers[:cycle] ||= Hash.new(0)
|
||||
|
||||
context.stack do
|
||||
key = context[@name]
|
||||
key = context.evaluate(@name)
|
||||
iteration = context.registers[:cycle][key]
|
||||
result = context[@variables[iteration]]
|
||||
result = context.evaluate(@variables[iteration])
|
||||
iteration += 1
|
||||
iteration = 0 if iteration >= @variables.size
|
||||
iteration = 0 if iteration >= @variables.size
|
||||
context.registers[:cycle][key] = iteration
|
||||
result
|
||||
end
|
||||
@@ -48,7 +48,7 @@ module Liquid
|
||||
def variables_from_string(markup)
|
||||
markup.split(',').collect do |var|
|
||||
var =~ /\s*(#{QuotedFragment})\s*/o
|
||||
$1 ? $1 : nil
|
||||
$1 ? Expression.parse($1) : nil
|
||||
end.compact
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
module Liquid
|
||||
|
||||
# decrement is used in a place where one needs to insert a counter
|
||||
# into a template, and needs the counter to survive across
|
||||
# multiple instantiations of the template.
|
||||
@@ -26,12 +25,10 @@ module Liquid
|
||||
|
||||
def render(context)
|
||||
value = context.environments.first[@variable] ||= 0
|
||||
value = value - 1
|
||||
value -= 1
|
||||
context.environments.first[@variable] = value
|
||||
value.to_s
|
||||
end
|
||||
|
||||
private
|
||||
end
|
||||
|
||||
Template.register_tag('decrement'.freeze, Decrement)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
module Liquid
|
||||
|
||||
# "For" iterates over an array or collection.
|
||||
# Several useful variables are available to you within the loop.
|
||||
#
|
||||
@@ -42,85 +41,41 @@ module Liquid
|
||||
# where 0 is the last item.
|
||||
# forloop.first:: Returns true if the item is the first item.
|
||||
# forloop.last:: Returns true if the item is the last item.
|
||||
# forloop.parentloop:: Provides access to the parent loop, if present.
|
||||
#
|
||||
class For < Block
|
||||
Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
@from = @limit = nil
|
||||
parse_with_selected_parser(markup)
|
||||
@nodelist = @for_block = []
|
||||
@for_block = BlockBody.new
|
||||
@else_block = nil
|
||||
end
|
||||
|
||||
def parse(tokens)
|
||||
return unless parse_body(@for_block, tokens)
|
||||
parse_body(@else_block, tokens)
|
||||
end
|
||||
|
||||
def nodelist
|
||||
if @else_block
|
||||
@for_block + @else_block
|
||||
else
|
||||
@for_block
|
||||
end
|
||||
@else_block ? [@for_block, @else_block] : [@for_block]
|
||||
end
|
||||
|
||||
def unknown_tag(tag, markup, tokens)
|
||||
return super unless tag == 'else'.freeze
|
||||
@nodelist = @else_block = []
|
||||
@else_block = BlockBody.new
|
||||
end
|
||||
|
||||
def render(context)
|
||||
context.registers[:for] ||= Hash.new(0)
|
||||
segment = collection_segment(context)
|
||||
|
||||
collection = context[@collection_name]
|
||||
collection = collection.to_a if collection.is_a?(Range)
|
||||
|
||||
# Maintains Ruby 1.8.7 String#each behaviour on 1.9
|
||||
return render_else(context) unless iterable?(collection)
|
||||
|
||||
from = if @attributes['offset'.freeze] == 'continue'.freeze
|
||||
context.registers[:for][@name].to_i
|
||||
if segment.empty?
|
||||
render_else(context)
|
||||
else
|
||||
context[@attributes['offset'.freeze]].to_i
|
||||
render_segment(context, segment)
|
||||
end
|
||||
|
||||
limit = context[@attributes['limit'.freeze]]
|
||||
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
|
||||
context.registers[:for][@name] = from + segment.length
|
||||
|
||||
context.stack do
|
||||
segment.each_with_index do |item, index|
|
||||
context[@variable_name] = item
|
||||
context['forloop'.freeze] = {
|
||||
'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)
|
||||
}
|
||||
|
||||
result << render_all(@for_block, context)
|
||||
|
||||
# Handle any interrupts if they exist.
|
||||
if context.has_interrupt?
|
||||
interrupt = context.pop_interrupt
|
||||
break if interrupt.is_a? BreakInterrupt
|
||||
next if interrupt.is_a? ContinueInterrupt
|
||||
end
|
||||
end
|
||||
end
|
||||
result
|
||||
end
|
||||
|
||||
protected
|
||||
@@ -128,12 +83,12 @@ module Liquid
|
||||
def lax_parse(markup)
|
||||
if markup =~ Syntax
|
||||
@variable_name = $1
|
||||
@collection_name = $2
|
||||
@name = "#{$1}-#{$2}"
|
||||
@reversed = $3
|
||||
@attributes = {}
|
||||
collection_name = $2
|
||||
@reversed = !!$3
|
||||
@name = "#{@variable_name}-#{collection_name}"
|
||||
@collection_name = Expression.parse(collection_name)
|
||||
markup.scan(TagAttributes) do |key, value|
|
||||
@attributes[key] = value
|
||||
set_attribute(key, value)
|
||||
end
|
||||
else
|
||||
raise SyntaxError.new(options[:locale].t("errors.syntax.for".freeze))
|
||||
@@ -143,31 +98,96 @@ module Liquid
|
||||
def strict_parse(markup)
|
||||
p = Parser.new(markup)
|
||||
@variable_name = p.consume(:id)
|
||||
raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_in".freeze)) unless p.id?('in'.freeze)
|
||||
@collection_name = p.expression
|
||||
@name = "#{@variable_name}-#{@collection_name}"
|
||||
raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_in".freeze)) unless p.id?('in'.freeze)
|
||||
collection_name = p.expression
|
||||
@name = "#{@variable_name}-#{collection_name}"
|
||||
@collection_name = Expression.parse(collection_name)
|
||||
@reversed = p.id?('reversed'.freeze)
|
||||
|
||||
@attributes = {}
|
||||
while p.look(:id) && p.look(:colon, 1)
|
||||
unless attribute = p.id?('limit'.freeze) || p.id?('offset'.freeze)
|
||||
raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_attribute".freeze))
|
||||
end
|
||||
p.consume
|
||||
val = p.expression
|
||||
@attributes[attribute] = val
|
||||
set_attribute(attribute, p.expression)
|
||||
end
|
||||
p.consume(:end_of_string)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def render_else(context)
|
||||
return @else_block ? [render_all(@else_block, context)] : ''.freeze
|
||||
def collection_segment(context)
|
||||
offsets = context.registers[:for] ||= Hash.new(0)
|
||||
|
||||
from = if @from == :continue
|
||||
offsets[@name].to_i
|
||||
else
|
||||
context.evaluate(@from).to_i
|
||||
end
|
||||
|
||||
collection = context.evaluate(@collection_name)
|
||||
collection = collection.to_a if collection.is_a?(Range)
|
||||
|
||||
limit = context.evaluate(@limit)
|
||||
to = limit ? limit.to_i + from : nil
|
||||
|
||||
segment = Utils.slice_collection(collection, from, to)
|
||||
segment.reverse! if @reversed
|
||||
|
||||
offsets[@name] = from + segment.length
|
||||
|
||||
segment
|
||||
end
|
||||
|
||||
def iterable?(collection)
|
||||
collection.respond_to?(:each) || Utils.non_blank_string?(collection)
|
||||
def render_segment(context, segment)
|
||||
for_stack = context.registers[:for_stack] ||= []
|
||||
length = segment.length
|
||||
|
||||
result = ''
|
||||
|
||||
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_with_index do |item, index|
|
||||
context[@variable_name] = item
|
||||
result << @for_block.render(context)
|
||||
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
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
def set_attribute(key, expr)
|
||||
case key
|
||||
when 'offset'.freeze
|
||||
@from = if expr == 'continue'.freeze
|
||||
:continue
|
||||
else
|
||||
Expression.parse(expr)
|
||||
end
|
||||
when 'limit'.freeze
|
||||
@limit = Expression.parse(expr)
|
||||
end
|
||||
end
|
||||
|
||||
def render_else(context)
|
||||
@else_block ? @else_block.render(context) : ''.freeze
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -20,8 +20,13 @@ module Liquid
|
||||
push_block('if'.freeze, markup)
|
||||
end
|
||||
|
||||
def parse(tokens)
|
||||
while parse_body(@blocks.last.attachment, tokens)
|
||||
end
|
||||
end
|
||||
|
||||
def nodelist
|
||||
@blocks.map(&:attachment).flatten
|
||||
@blocks.map(&:attachment)
|
||||
end
|
||||
|
||||
def unknown_tag(tag, markup, tokens)
|
||||
@@ -36,7 +41,7 @@ module Liquid
|
||||
context.stack do
|
||||
@blocks.each do |block|
|
||||
if block.evaluate(context)
|
||||
return render_all(block.attachment, context)
|
||||
return block.attachment.render(context)
|
||||
end
|
||||
end
|
||||
''.freeze
|
||||
@@ -45,61 +50,61 @@ module Liquid
|
||||
|
||||
private
|
||||
|
||||
def push_block(tag, markup)
|
||||
block = if tag == 'else'.freeze
|
||||
ElseCondition.new
|
||||
else
|
||||
parse_with_selected_parser(markup)
|
||||
end
|
||||
|
||||
@blocks.push(block)
|
||||
@nodelist = block.attach(Array.new)
|
||||
def push_block(tag, markup)
|
||||
block = if tag == 'else'.freeze
|
||||
ElseCondition.new
|
||||
else
|
||||
parse_with_selected_parser(markup)
|
||||
end
|
||||
|
||||
def lax_parse(markup)
|
||||
expressions = markup.scan(ExpressionsAndOperators).reverse
|
||||
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.shift =~ Syntax
|
||||
@blocks.push(block)
|
||||
block.attach(BlockBody.new)
|
||||
end
|
||||
|
||||
condition = Condition.new($1, $2, $3)
|
||||
def lax_parse(markup)
|
||||
expressions = markup.scan(ExpressionsAndOperators)
|
||||
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop =~ Syntax
|
||||
|
||||
while not expressions.empty?
|
||||
operator = (expressions.shift).to_s.strip
|
||||
condition = Condition.new(Expression.parse($1), $2, Expression.parse($3))
|
||||
|
||||
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.shift.to_s =~ Syntax
|
||||
until expressions.empty?
|
||||
operator = expressions.pop.to_s.strip
|
||||
|
||||
new_condition = Condition.new($1, $2, $3)
|
||||
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless BOOLEAN_OPERATORS.include?(operator)
|
||||
new_condition.send(operator, condition)
|
||||
condition = new_condition
|
||||
end
|
||||
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop.to_s =~ Syntax
|
||||
|
||||
condition
|
||||
new_condition = Condition.new(Expression.parse($1), $2, Expression.parse($3))
|
||||
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless BOOLEAN_OPERATORS.include?(operator)
|
||||
new_condition.send(operator, condition)
|
||||
condition = new_condition
|
||||
end
|
||||
|
||||
def strict_parse(markup)
|
||||
p = Parser.new(markup)
|
||||
condition
|
||||
end
|
||||
|
||||
condition = parse_comparison(p)
|
||||
def strict_parse(markup)
|
||||
p = Parser.new(markup)
|
||||
condition = parse_binary_comparison(p)
|
||||
p.consume(:end_of_string)
|
||||
condition
|
||||
end
|
||||
|
||||
while op = (p.id?('and'.freeze) || p.id?('or'.freeze))
|
||||
new_cond = parse_comparison(p)
|
||||
new_cond.send(op, condition)
|
||||
condition = new_cond
|
||||
end
|
||||
p.consume(:end_of_string)
|
||||
|
||||
condition
|
||||
def parse_binary_comparison(p)
|
||||
condition = parse_comparison(p)
|
||||
if op = (p.id?('and'.freeze) || p.id?('or'.freeze))
|
||||
condition.send(op, parse_binary_comparison(p))
|
||||
end
|
||||
condition
|
||||
end
|
||||
|
||||
def parse_comparison(p)
|
||||
a = p.expression
|
||||
if op = p.consume?(:comparison)
|
||||
b = p.expression
|
||||
Condition.new(a, op, b)
|
||||
else
|
||||
Condition.new(a)
|
||||
end
|
||||
def parse_comparison(p)
|
||||
a = Expression.parse(p.expression)
|
||||
if op = p.consume?(:comparison)
|
||||
b = Expression.parse(p.expression)
|
||||
Condition.new(a, op, b)
|
||||
else
|
||||
Condition.new(a)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('if'.freeze, If)
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
module Liquid
|
||||
class Ifchanged < Block
|
||||
|
||||
def render(context)
|
||||
context.stack do
|
||||
|
||||
output = render_all(@nodelist, context)
|
||||
output = super
|
||||
|
||||
if output != context.registers[:ifchanged]
|
||||
context.registers[:ifchanged] = output
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
module Liquid
|
||||
|
||||
# Include allows templates to relate with other templates
|
||||
#
|
||||
# Simply include another template:
|
||||
@@ -22,12 +21,15 @@ module Liquid
|
||||
|
||||
if markup =~ Syntax
|
||||
|
||||
@template_name = $1
|
||||
@variable_name = $3
|
||||
@attributes = {}
|
||||
template_name = $1
|
||||
variable_name = $3
|
||||
|
||||
@variable_name_expr = variable_name ? Expression.parse(variable_name) : nil
|
||||
@template_name_expr = Expression.parse(template_name)
|
||||
@attributes = {}
|
||||
|
||||
markup.scan(TagAttributes) do |key, value|
|
||||
@attributes[key] = value
|
||||
@attributes[key] = Expression.parse(value)
|
||||
end
|
||||
|
||||
else
|
||||
@@ -35,59 +37,75 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
def parse(tokens)
|
||||
def parse(_tokens)
|
||||
end
|
||||
|
||||
def render(context)
|
||||
partial = load_cached_partial(context)
|
||||
variable = context[@variable_name || @template_name[1..-2]]
|
||||
template_name = context.evaluate(@template_name_expr)
|
||||
partial = load_cached_partial(template_name, context)
|
||||
|
||||
context.stack do
|
||||
@attributes.each do |key, value|
|
||||
context[key] = context[value]
|
||||
end
|
||||
context_variable_name = template_name.split('/'.freeze).last
|
||||
|
||||
context_variable_name = @template_name[1..-2].split('/'.freeze).last
|
||||
if variable.is_a?(Array)
|
||||
variable.collect do |var|
|
||||
context[context_variable_name] = var
|
||||
variable = if @variable_name_expr
|
||||
context.evaluate(@variable_name_expr)
|
||||
else
|
||||
context.find_variable(template_name)
|
||||
end
|
||||
|
||||
old_template_name = context.template_name
|
||||
old_partial = context.partial
|
||||
begin
|
||||
context.template_name = template_name
|
||||
context.partial = true
|
||||
context.stack do
|
||||
@attributes.each do |key, value|
|
||||
context[key] = context.evaluate(value)
|
||||
end
|
||||
|
||||
if variable.is_a?(Array)
|
||||
variable.collect do |var|
|
||||
context[context_variable_name] = var
|
||||
partial.render(context)
|
||||
end
|
||||
else
|
||||
context[context_variable_name] = variable
|
||||
partial.render(context)
|
||||
end
|
||||
else
|
||||
context[context_variable_name] = variable
|
||||
partial.render(context)
|
||||
end
|
||||
ensure
|
||||
context.template_name = old_template_name
|
||||
context.partial = old_partial
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def load_cached_partial(context)
|
||||
cached_partials = context.registers[:cached_partials] || {}
|
||||
template_name = context[@template_name]
|
||||
|
||||
if cached = cached_partials[template_name]
|
||||
return cached
|
||||
end
|
||||
source = read_template_from_file_system(context)
|
||||
partial = Liquid::Template.parse(source)
|
||||
cached_partials[template_name] = partial
|
||||
context.registers[:cached_partials] = cached_partials
|
||||
partial
|
||||
alias_method :parse_context, :options
|
||||
private :parse_context
|
||||
|
||||
def load_cached_partial(template_name, context)
|
||||
cached_partials = context.registers[:cached_partials] || {}
|
||||
|
||||
if cached = cached_partials[template_name]
|
||||
return cached
|
||||
end
|
||||
|
||||
def read_template_from_file_system(context)
|
||||
file_system = context.registers[:file_system] || Liquid::Template.file_system
|
||||
|
||||
# make read_template_file call backwards-compatible.
|
||||
case file_system.method(:read_template_file).arity
|
||||
when 1
|
||||
file_system.read_template_file(context[@template_name])
|
||||
when 2
|
||||
file_system.read_template_file(context[@template_name], context)
|
||||
else
|
||||
raise ArgumentError, "file_system.read_template_file expects two parameters: (template_name, context)"
|
||||
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
|
||||
|
||||
Template.register_tag('include'.freeze, Include)
|
||||
|
||||
@@ -1,20 +1,39 @@
|
||||
module Liquid
|
||||
class Raw < Block
|
||||
Syntax = /\A\s*\z/
|
||||
FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
|
||||
|
||||
def initialize(tag_name, markup, parse_context)
|
||||
super
|
||||
|
||||
unless markup =~ Syntax
|
||||
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_unexpected_args".freeze, tag: tag_name))
|
||||
end
|
||||
end
|
||||
|
||||
def parse(tokens)
|
||||
@nodelist ||= []
|
||||
@nodelist.clear
|
||||
@body = ''
|
||||
while token = tokens.shift
|
||||
if token =~ FullTokenPossiblyInvalid
|
||||
@nodelist << $1 if $1 != "".freeze
|
||||
if @block_delimiter == $2
|
||||
end_tag
|
||||
return
|
||||
end
|
||||
@body << $1 if $1 != "".freeze
|
||||
return if block_delimiter == $2
|
||||
end
|
||||
@nodelist << token if not token.empty?
|
||||
@body << token unless token.empty?
|
||||
end
|
||||
|
||||
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
|
||||
end
|
||||
|
||||
def render(_context)
|
||||
@body
|
||||
end
|
||||
|
||||
def nodelist
|
||||
[@body]
|
||||
end
|
||||
|
||||
def blank?
|
||||
@body.empty?
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -6,10 +6,10 @@ module Liquid
|
||||
super
|
||||
if markup =~ Syntax
|
||||
@variable_name = $1
|
||||
@collection_name = $2
|
||||
@collection_name = Expression.parse($2)
|
||||
@attributes = {}
|
||||
markup.scan(TagAttributes) do |key, value|
|
||||
@attributes[key] = value
|
||||
@attributes[key] = Expression.parse(value)
|
||||
end
|
||||
else
|
||||
raise SyntaxError.new(options[:locale].t("errors.syntax.table_row".freeze))
|
||||
@@ -17,51 +17,32 @@ module Liquid
|
||||
end
|
||||
|
||||
def render(context)
|
||||
collection = context[@collection_name] or return ''.freeze
|
||||
collection = context.evaluate(@collection_name) or return ''.freeze
|
||||
|
||||
from = @attributes['offset'.freeze] ? context[@attributes['offset'.freeze]].to_i : 0
|
||||
to = @attributes['limit'.freeze] ? from + context[@attributes['limit'.freeze]].to_i : nil
|
||||
from = @attributes.key?('offset'.freeze) ? context.evaluate(@attributes['offset'.freeze]).to_i : 0
|
||||
to = @attributes.key?('limit'.freeze) ? from + context.evaluate(@attributes['limit'.freeze]).to_i : nil
|
||||
|
||||
collection = Utils.slice_collection(collection, from, to)
|
||||
|
||||
length = collection.length
|
||||
|
||||
cols = context[@attributes['cols'.freeze]].to_i
|
||||
|
||||
row = 1
|
||||
col = 0
|
||||
cols = context.evaluate(@attributes['cols'.freeze]).to_i
|
||||
|
||||
result = "<tr class=\"row1\">\n"
|
||||
context.stack do
|
||||
tablerowloop = Liquid::TablerowloopDrop.new(length, cols)
|
||||
context['tablerowloop'.freeze] = tablerowloop
|
||||
|
||||
collection.each_with_index do |item, index|
|
||||
context[@variable_name] = item
|
||||
context['tablerowloop'.freeze] = {
|
||||
'length'.freeze => length,
|
||||
'index'.freeze => index + 1,
|
||||
'index0'.freeze => index,
|
||||
'col'.freeze => col + 1,
|
||||
'col0'.freeze => col,
|
||||
'index0'.freeze => index,
|
||||
'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)
|
||||
}
|
||||
|
||||
result << "<td class=\"col#{tablerowloop.col}\">" << super << '</td>'
|
||||
|
||||
col += 1
|
||||
|
||||
result << "<td class=\"col#{col}\">" << render_all(@nodelist, context) << '</td>'
|
||||
|
||||
if col == cols and (index != length - 1)
|
||||
col = 0
|
||||
row += 1
|
||||
result << "</tr>\n<tr class=\"row#{row}\">"
|
||||
if tablerowloop.col_last && !tablerowloop.last
|
||||
result << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">"
|
||||
end
|
||||
|
||||
tablerowloop.send(:increment!)
|
||||
end
|
||||
end
|
||||
result << "</tr>\n"
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
require File.dirname(__FILE__) + '/if'
|
||||
require_relative 'if'
|
||||
|
||||
module Liquid
|
||||
# Unless is a conditional just like 'if' but works on the inverse logic.
|
||||
#
|
||||
# {% unless x < 0 %} x is greater than zero {% end %}
|
||||
# {% unless x < 0 %} x is greater than zero {% endunless %}
|
||||
#
|
||||
class Unless < If
|
||||
def render(context)
|
||||
context.stack do
|
||||
|
||||
# First condition is interpreted backwards ( if not )
|
||||
first_block = @blocks.first
|
||||
unless first_block.evaluate(context)
|
||||
return render_all(first_block.attachment, context)
|
||||
return first_block.attachment.render(context)
|
||||
end
|
||||
|
||||
# After the first condition unless works just like if
|
||||
@blocks[1..-1].each do |block|
|
||||
if block.evaluate(context)
|
||||
return render_all(block.attachment, context)
|
||||
return block.attachment.render(context)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
module Liquid
|
||||
|
||||
# Templates are central to liquid.
|
||||
# Interpretating templates is a two step process. First you compile the
|
||||
# source code you got. During compile time some extensive error checking is performed.
|
||||
@@ -14,21 +13,19 @@ module Liquid
|
||||
# template.render('user_name' => 'bob')
|
||||
#
|
||||
class Template
|
||||
DEFAULT_OPTIONS = {
|
||||
:locale => I18n.new
|
||||
}
|
||||
attr_accessor :root
|
||||
attr_reader :resource_limits, :warnings
|
||||
|
||||
attr_accessor :root, :resource_limits
|
||||
@@file_system = BlankFileSystem.new
|
||||
|
||||
class TagRegistry
|
||||
def initialize
|
||||
@tags = {}
|
||||
@tags = {}
|
||||
@cache = {}
|
||||
end
|
||||
|
||||
def [](tag_name)
|
||||
return nil unless @tags.has_key?(tag_name)
|
||||
return nil unless @tags.key?(tag_name)
|
||||
return @cache[tag_name] if Liquid.cache_classes
|
||||
|
||||
lookup_class(@tags[tag_name]).tap { |o| @cache[tag_name] = o }
|
||||
@@ -51,6 +48,8 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :profiler
|
||||
|
||||
class << self
|
||||
# Sets how strict the parser should be.
|
||||
# :lax acts like liquid 2.5 and silently ignores malformed tags in most cases.
|
||||
@@ -58,6 +57,12 @@ module Liquid
|
||||
# :strict will enforce correct syntax.
|
||||
attr_writer :error_mode
|
||||
|
||||
# Sets how strict the taint checker should be.
|
||||
# :lax is the default, and ignores the taint flag completely
|
||||
# :warn adds a warning, but does not interrupt the rendering
|
||||
# :error raises an error when tainted output is used
|
||||
attr_writer :taint_mode
|
||||
|
||||
def file_system
|
||||
@@file_system
|
||||
end
|
||||
@@ -75,7 +80,11 @@ module Liquid
|
||||
end
|
||||
|
||||
def error_mode
|
||||
@error_mode || :lax
|
||||
@error_mode ||= :lax
|
||||
end
|
||||
|
||||
def taint_mode
|
||||
@taint_mode ||= :lax
|
||||
end
|
||||
|
||||
# Pass a module with filter methods which should be available
|
||||
@@ -84,31 +93,36 @@ module Liquid
|
||||
Strainer.global_filter(mod)
|
||||
end
|
||||
|
||||
def default_resource_limits
|
||||
@default_resource_limits ||= {}
|
||||
end
|
||||
|
||||
# creates a new <tt>Template</tt> object from liquid source code
|
||||
# To enable profiling, pass in <tt>profile: true</tt> as an option.
|
||||
# See Liquid::Profiler for more information
|
||||
def parse(source, options = {})
|
||||
template = Template.new
|
||||
template.parse(source, options)
|
||||
end
|
||||
end
|
||||
|
||||
# creates a new <tt>Template</tt> from an array of tokens. Use <tt>Template.parse</tt> instead
|
||||
def initialize
|
||||
@resource_limits = {}
|
||||
@rethrow_errors = false
|
||||
@resource_limits = ResourceLimits.new(self.class.default_resource_limits)
|
||||
end
|
||||
|
||||
# Parse source code.
|
||||
# Returns self for easy chaining
|
||||
def parse(source, options = {})
|
||||
@root = Document.parse(tokenize(source), DEFAULT_OPTIONS.merge(options))
|
||||
@warnings = nil
|
||||
@options = options
|
||||
@profiling = options[:profile]
|
||||
@line_numbers = options[:line_numbers] || @profiling
|
||||
parse_context = options.is_a?(ParseContext) ? options : ParseContext.new(options)
|
||||
@root = Document.parse(tokenize(source), parse_context)
|
||||
@warnings = parse_context.warnings
|
||||
self
|
||||
end
|
||||
|
||||
def warnings
|
||||
return [] unless @root
|
||||
@warnings ||= @root.warnings
|
||||
end
|
||||
|
||||
def registers
|
||||
@registers ||= {}
|
||||
end
|
||||
@@ -130,6 +144,9 @@ module Liquid
|
||||
# if you use the same filters over and over again consider registering them globally
|
||||
# with <tt>Template.register_filter</tt>
|
||||
#
|
||||
# if profiling was enabled in <tt>Template#parse</tt> then the resulting profiling information
|
||||
# will be available via <tt>Template#profiler</tt>
|
||||
#
|
||||
# Following options can be passed:
|
||||
#
|
||||
# * <tt>filters</tt> : array with local filters
|
||||
@@ -142,7 +159,11 @@ module Liquid
|
||||
context = case args.first
|
||||
when Liquid::Context
|
||||
c = args.shift
|
||||
c.rethrow_errors = true if @rethrow_errors
|
||||
|
||||
if @rethrow_errors
|
||||
c.exception_handler = ->(e) { raise }
|
||||
end
|
||||
|
||||
c
|
||||
when Liquid::Drop
|
||||
drop = args.shift
|
||||
@@ -159,24 +180,22 @@ module Liquid
|
||||
when Hash
|
||||
options = args.pop
|
||||
|
||||
if options[:registers].is_a?(Hash)
|
||||
self.registers.merge!(options[:registers])
|
||||
end
|
||||
registers.merge!(options[:registers]) if options[:registers].is_a?(Hash)
|
||||
|
||||
if options[:filters]
|
||||
context.add_filters(options[:filters])
|
||||
end
|
||||
|
||||
when Module
|
||||
context.add_filters(args.pop)
|
||||
when Array
|
||||
apply_options_to_context(context, options)
|
||||
when Module, Array
|
||||
context.add_filters(args.pop)
|
||||
end
|
||||
|
||||
# Retrying a render resets resource usage
|
||||
context.resource_limits.reset
|
||||
|
||||
begin
|
||||
# render the nodelist.
|
||||
# for performance reasons we get an array back here. join will make a string out of it.
|
||||
result = @root.render(context)
|
||||
result = with_profiling(context) do
|
||||
@root.render(context)
|
||||
end
|
||||
result.respond_to?(:join) ? result.join : result
|
||||
rescue Liquid::MemoryError => e
|
||||
context.handle_error(e)
|
||||
@@ -192,17 +211,33 @@ module Liquid
|
||||
|
||||
private
|
||||
|
||||
# Uses the <tt>Liquid::TemplateParser</tt> regexp to tokenize the passed source
|
||||
def tokenize(source)
|
||||
source = source.source if source.respond_to?(:source)
|
||||
return [] if source.to_s.empty?
|
||||
tokens = source.split(TemplateParser)
|
||||
|
||||
# removes the rogue empty element at the beginning of the array
|
||||
tokens.shift if tokens[0] and tokens[0].empty?
|
||||
|
||||
tokens
|
||||
Tokenizer.new(source, @line_numbers)
|
||||
end
|
||||
|
||||
def with_profiling(context)
|
||||
if @profiling && !context.partial
|
||||
raise "Profiler not loaded, require 'liquid/profiler' first" unless defined?(Liquid::Profiler)
|
||||
|
||||
@profiler = Profiler.new
|
||||
@profiler.start
|
||||
|
||||
begin
|
||||
yield
|
||||
ensure
|
||||
@profiler.stop
|
||||
end
|
||||
else
|
||||
yield
|
||||
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_handler = options[:exception_handler] if options[:exception_handler]
|
||||
context.strict_variables = options[:strict_variables] if options[:strict_variables]
|
||||
context.strict_filters = options[:strict_filters] if options[:strict_filters]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
31
lib/liquid/tokenizer.rb
Normal file
31
lib/liquid/tokenizer.rb
Normal file
@@ -0,0 +1,31 @@
|
||||
module Liquid
|
||||
class Tokenizer
|
||||
attr_reader :line_number
|
||||
|
||||
def initialize(source, line_numbers = false)
|
||||
@source = source
|
||||
@line_number = line_numbers ? 1 : nil
|
||||
@tokens = tokenize
|
||||
end
|
||||
|
||||
def shift
|
||||
token = @tokens.shift
|
||||
@line_number += token.count("\n") if @line_number && token
|
||||
token
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def tokenize
|
||||
@source = @source.source if @source.respond_to?(:source)
|
||||
return [] if @source.to_s.empty?
|
||||
|
||||
tokens = @source.split(TemplateParser)
|
||||
|
||||
# removes the rogue empty element at the beginning of the array
|
||||
tokens.shift if tokens[0] && tokens[0].empty?
|
||||
|
||||
tokens
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,27 +1,24 @@
|
||||
module Liquid
|
||||
module Utils
|
||||
|
||||
def self.slice_collection(collection, from, to)
|
||||
if (from != 0 || to != nil) && collection.respond_to?(:load_slice)
|
||||
if (from != 0 || !to.nil?) && collection.respond_to?(:load_slice)
|
||||
collection.load_slice(from, to)
|
||||
else
|
||||
slice_collection_using_each(collection, from, to)
|
||||
end
|
||||
end
|
||||
|
||||
def self.non_blank_string?(collection)
|
||||
collection.is_a?(String) && collection != ''.freeze
|
||||
end
|
||||
|
||||
def self.slice_collection_using_each(collection, from, to)
|
||||
segments = []
|
||||
index = 0
|
||||
|
||||
# Maintains Ruby 1.8.7 String#each behaviour on 1.9
|
||||
return [collection] if non_blank_string?(collection)
|
||||
if collection.is_a?(String)
|
||||
return collection.empty? ? [] : [collection]
|
||||
end
|
||||
return [] unless collection.respond_to?(:each)
|
||||
|
||||
collection.each do |item|
|
||||
|
||||
if to && to <= index
|
||||
break
|
||||
end
|
||||
@@ -35,5 +32,52 @@ module Liquid
|
||||
|
||||
segments
|
||||
end
|
||||
|
||||
def self.to_integer(num)
|
||||
return num if num.is_a?(Integer)
|
||||
num = num.to_s
|
||||
begin
|
||||
Integer(num)
|
||||
rescue ::ArgumentError
|
||||
raise Liquid::ArgumentError, "invalid integer"
|
||||
end
|
||||
end
|
||||
|
||||
def self.to_number(obj)
|
||||
case obj
|
||||
when Float
|
||||
BigDecimal.new(obj.to_s)
|
||||
when Numeric
|
||||
obj
|
||||
when String
|
||||
(obj.strip =~ /\A-?\d+\.\d+\z/) ? BigDecimal.new(obj) : obj.to_i
|
||||
else
|
||||
if obj.respond_to?(:to_number)
|
||||
obj.to_number
|
||||
else
|
||||
0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.to_date(obj)
|
||||
return obj if obj.respond_to?(:strftime)
|
||||
|
||||
if obj.is_a?(String)
|
||||
return nil if obj.empty?
|
||||
obj = obj.downcase
|
||||
end
|
||||
|
||||
case obj
|
||||
when 'now'.freeze, 'today'.freeze
|
||||
Time.now
|
||||
when /\A\d+\z/, Integer
|
||||
Time.at(obj.to_i)
|
||||
when String
|
||||
Time.parse(obj)
|
||||
end
|
||||
rescue ::ArgumentError
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
module Liquid
|
||||
|
||||
# Holds variables. Variables are only loaded "just in time"
|
||||
# and are not evaluated as part of the render stage
|
||||
#
|
||||
@@ -11,97 +10,126 @@ module Liquid
|
||||
# {{ user | link }}
|
||||
#
|
||||
class Variable
|
||||
FilterParser = /(?:#{FilterSeparator}|(?:\s*(?:#{QuotedFragment}|#{ArgumentSeparator})\s*)+)/o
|
||||
EasyParse = /\A *(\w+(?:\.\w+)*) *\z/
|
||||
attr_accessor :filters, :name, :warnings
|
||||
FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
|
||||
attr_accessor :filters, :name, :line_number
|
||||
attr_reader :parse_context
|
||||
alias_method :options, :parse_context
|
||||
include ParserSwitching
|
||||
|
||||
def initialize(markup, options = {})
|
||||
def initialize(markup, parse_context)
|
||||
@markup = markup
|
||||
@name = nil
|
||||
@options = options || {}
|
||||
@parse_context = parse_context
|
||||
@line_number = parse_context.line_number
|
||||
|
||||
case @options[:error_mode] || Template.error_mode
|
||||
when :strict then strict_parse(markup)
|
||||
when :lax then lax_parse(markup)
|
||||
when :warn
|
||||
begin
|
||||
strict_parse(markup)
|
||||
rescue SyntaxError => e
|
||||
@warnings ||= []
|
||||
@warnings << e
|
||||
lax_parse(markup)
|
||||
end
|
||||
end
|
||||
parse_with_selected_parser(markup)
|
||||
end
|
||||
|
||||
def raw
|
||||
@markup
|
||||
end
|
||||
|
||||
def markup_context(markup)
|
||||
"in \"{{#{markup}}}\""
|
||||
end
|
||||
|
||||
def lax_parse(markup)
|
||||
@filters = []
|
||||
if markup =~ /\s*(#{QuotedFragment})(.*)/om
|
||||
@name = Regexp.last_match(1)
|
||||
if Regexp.last_match(2) =~ /#{FilterSeparator}\s*(.*)/om
|
||||
filters = Regexp.last_match(1).scan(FilterParser)
|
||||
filters.each do |f|
|
||||
if f =~ /\s*(\w+)/
|
||||
filtername = Regexp.last_match(1)
|
||||
filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o).flatten
|
||||
@filters << [filtername, filterargs]
|
||||
end
|
||||
end
|
||||
return unless markup =~ /(#{QuotedFragment})(.*)/om
|
||||
|
||||
name_markup = $1
|
||||
filter_markup = $2
|
||||
@name = Expression.parse(name_markup)
|
||||
if filter_markup =~ /#{FilterSeparator}\s*(.*)/om
|
||||
filters = $1.scan(FilterParser)
|
||||
filters.each do |f|
|
||||
next unless f =~ /\w+/
|
||||
filtername = Regexp.last_match(0)
|
||||
filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o).flatten
|
||||
@filters << parse_filter_expressions(filtername, filterargs)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def strict_parse(markup)
|
||||
# Very simple valid cases
|
||||
if markup =~ EasyParse
|
||||
@name = $1
|
||||
@filters = []
|
||||
return
|
||||
end
|
||||
|
||||
@filters = []
|
||||
p = Parser.new(markup)
|
||||
# Could be just filters with no input
|
||||
@name = p.look(:pipe) ? ''.freeze : p.expression
|
||||
|
||||
@name = Expression.parse(p.expression)
|
||||
while p.consume?(:pipe)
|
||||
filtername = p.consume(:id)
|
||||
filterargs = p.consume?(:colon) ? parse_filterargs(p) : []
|
||||
@filters << [filtername, filterargs]
|
||||
@filters << parse_filter_expressions(filtername, filterargs)
|
||||
end
|
||||
p.consume(:end_of_string)
|
||||
rescue SyntaxError => e
|
||||
e.message << " in \"{{#{markup}}}\""
|
||||
raise e
|
||||
end
|
||||
|
||||
def parse_filterargs(p)
|
||||
# first argument
|
||||
filterargs = [p.argument]
|
||||
# followed by comma separated others
|
||||
while p.consume?(:comma)
|
||||
filterargs << p.argument
|
||||
end
|
||||
filterargs << p.argument while p.consume?(:comma)
|
||||
filterargs
|
||||
end
|
||||
|
||||
def render(context)
|
||||
return ''.freeze if @name.nil?
|
||||
@filters.inject(context[@name]) do |output, filter|
|
||||
filterargs = []
|
||||
keyword_args = {}
|
||||
filter[1].to_a.each do |a|
|
||||
if matches = a.match(/\A#{TagAttributes}\z/o)
|
||||
keyword_args[matches[1]] = context[matches[2]]
|
||||
else
|
||||
filterargs << context[a]
|
||||
end
|
||||
obj = @filters.inject(context.evaluate(@name)) do |output, (filter_name, filter_args, filter_kwargs)|
|
||||
filter_args = evaluate_filter_expressions(context, filter_args, filter_kwargs)
|
||||
context.invoke(filter_name, output, *filter_args)
|
||||
end
|
||||
|
||||
obj = context.apply_global_filter(obj)
|
||||
|
||||
taint_check(context, obj)
|
||||
|
||||
obj
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_filter_expressions(filter_name, unparsed_args)
|
||||
filter_args = []
|
||||
keyword_args = {}
|
||||
unparsed_args.each do |a|
|
||||
if matches = a.match(/\A#{TagAttributes}\z/o)
|
||||
keyword_args[matches[1]] = Expression.parse(matches[2])
|
||||
else
|
||||
filter_args << Expression.parse(a)
|
||||
end
|
||||
filterargs << keyword_args unless keyword_args.empty?
|
||||
begin
|
||||
output = context.invoke(filter[0], output, *filterargs)
|
||||
rescue FilterNotFound
|
||||
raise FilterNotFound, "Error - filter '#{filter[0]}' in '#{@markup.strip}' could not be found."
|
||||
end
|
||||
result = [filter_name, filter_args]
|
||||
result << keyword_args unless keyword_args.empty?
|
||||
result
|
||||
end
|
||||
|
||||
def evaluate_filter_expressions(context, filter_args, filter_kwargs)
|
||||
parsed_args = filter_args.map{ |expr| context.evaluate(expr) }
|
||||
if filter_kwargs
|
||||
parsed_kwargs = {}
|
||||
filter_kwargs.each do |key, expr|
|
||||
parsed_kwargs[key] = context.evaluate(expr)
|
||||
end
|
||||
parsed_args << parsed_kwargs
|
||||
end
|
||||
parsed_args
|
||||
end
|
||||
|
||||
def taint_check(context, obj)
|
||||
return unless obj.tainted?
|
||||
return if Template.taint_mode == :lax
|
||||
|
||||
@markup =~ QuotedFragment
|
||||
name = Regexp.last_match(0)
|
||||
|
||||
error = TaintedError.new("variable '#{name}' is tainted and was not escaped")
|
||||
error.line_number = line_number
|
||||
error.template_name = context.template_name
|
||||
|
||||
case Template.taint_mode
|
||||
when :warn
|
||||
context.warnings << error
|
||||
when :error
|
||||
raise error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
82
lib/liquid/variable_lookup.rb
Normal file
82
lib/liquid/variable_lookup.rb
Normal file
@@ -0,0 +1,82 @@
|
||||
module Liquid
|
||||
class VariableLookup
|
||||
SQUARE_BRACKETED = /\A\[(.*)\]\z/m
|
||||
COMMAND_METHODS = ['size'.freeze, 'first'.freeze, 'last'.freeze]
|
||||
|
||||
attr_reader :name, :lookups
|
||||
|
||||
def self.parse(markup)
|
||||
new(markup)
|
||||
end
|
||||
|
||||
def initialize(markup)
|
||||
lookups = markup.scan(VariableParser)
|
||||
|
||||
name = lookups.shift
|
||||
if name =~ SQUARE_BRACKETED
|
||||
name = Expression.parse($1)
|
||||
end
|
||||
@name = name
|
||||
|
||||
@lookups = lookups
|
||||
@command_flags = 0
|
||||
|
||||
@lookups.each_index do |i|
|
||||
lookup = lookups[i]
|
||||
if lookup =~ SQUARE_BRACKETED
|
||||
lookups[i] = Expression.parse($1)
|
||||
elsif COMMAND_METHODS.include?(lookup)
|
||||
@command_flags |= 1 << i
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def evaluate(context)
|
||||
name = context.evaluate(@name)
|
||||
object = context.find_variable(name)
|
||||
|
||||
@lookups.each_index do |i|
|
||||
key = context.evaluate(@lookups[i])
|
||||
|
||||
# If object is a hash- or array-like object we look for the
|
||||
# presence of the key and if its available we return it
|
||||
if object.respond_to?(:[]) &&
|
||||
((object.respond_to?(:key?) && object.key?(key)) ||
|
||||
(object.respond_to?(:fetch) && key.is_a?(Integer)))
|
||||
|
||||
# if its a proc we will replace the entry with the proc
|
||||
res = context.lookup_and_evaluate(object, key)
|
||||
object = res.to_liquid
|
||||
|
||||
# Some special cases. If the part wasn't in square brackets and
|
||||
# no key with the same name was found we interpret following calls
|
||||
# as commands and call them on the current object
|
||||
elsif @command_flags & (1 << i) != 0 && object.respond_to?(key)
|
||||
object = object.send(key).to_liquid
|
||||
|
||||
# No key was present with the desired value and it wasn't one of the directly supported
|
||||
# keywords either. The only thing we got left is to return nil or
|
||||
# raise an exception if `strict_variables` option is set to true
|
||||
else
|
||||
return nil unless context.strict_variables
|
||||
raise Liquid::UndefinedVariable, "undefined variable #{key}"
|
||||
end
|
||||
|
||||
# If we are dealing with a drop here we have to
|
||||
object.context = context if object.respond_to?(:context=)
|
||||
end
|
||||
|
||||
object
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
self.class == other.class && state == other.state
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def state
|
||||
[@name, @lookups, @command_flags]
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,4 +1,4 @@
|
||||
# encoding: utf-8
|
||||
module Liquid
|
||||
VERSION = "3.0.0"
|
||||
VERSION = "4.0.0.rc2"
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# encoding: utf-8
|
||||
lib = File.expand_path('../lib/', __FILE__)
|
||||
$:.unshift lib unless $:.include?(lib)
|
||||
$LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib)
|
||||
|
||||
require "liquid/version"
|
||||
|
||||
@@ -9,20 +9,21 @@ Gem::Specification.new do |s|
|
||||
s.version = Liquid::VERSION
|
||||
s.platform = Gem::Platform::RUBY
|
||||
s.summary = "A secure, non-evaling end user template engine with aesthetic markup."
|
||||
s.authors = ["Tobias Luetke"]
|
||||
s.authors = ["Tobias Lütke"]
|
||||
s.email = ["tobi@leetsoft.com"]
|
||||
s.homepage = "http://www.liquidmarkup.org"
|
||||
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_rubygems_version = ">= 1.3.7"
|
||||
|
||||
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.add_development_dependency 'rake'
|
||||
s.add_development_dependency 'minitest'
|
||||
end
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
require 'benchmark'
|
||||
require File.dirname(__FILE__) + '/theme_runner'
|
||||
require 'benchmark/ips'
|
||||
require_relative 'theme_runner'
|
||||
|
||||
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
|
||||
profiler = ThemeRunner.new
|
||||
|
||||
Benchmark.bmbm do |x|
|
||||
x.report("parse:") { 100.times { profiler.compile } }
|
||||
x.report("parse & run:") { 100.times { profiler.run } }
|
||||
end
|
||||
Benchmark.ips do |x|
|
||||
x.time = 60
|
||||
x.warmup = 5
|
||||
|
||||
puts
|
||||
puts "Running benchmark for #{x.time} seconds (with #{x.warmup} seconds warmup)."
|
||||
puts
|
||||
|
||||
x.report("parse:") { profiler.compile }
|
||||
x.report("parse & run:") { profiler.run }
|
||||
end
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
require 'stackprof' rescue fail("install stackprof extension/gem")
|
||||
require File.dirname(__FILE__) + '/theme_runner'
|
||||
require 'stackprof'
|
||||
require_relative 'theme_runner'
|
||||
|
||||
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
|
||||
profiler = ThemeRunner.new
|
||||
profiler.run
|
||||
|
||||
[:cpu, :object].each do |profile_type|
|
||||
puts "Profiling in #{profile_type.to_s} mode..."
|
||||
puts "Profiling in #{profile_type} mode..."
|
||||
results = StackProf.run(mode: profile_type) do
|
||||
100.times do
|
||||
200.times do
|
||||
profiler.run
|
||||
end
|
||||
end
|
||||
|
||||
if profile_type == :cpu && graph_filename = ENV['GRAPH_FILENAME']
|
||||
File.open(graph_filename, 'w') do |f|
|
||||
StackProf::Report.new(results).print_graphviz(nil, f)
|
||||
end
|
||||
end
|
||||
|
||||
StackProf::Report.new(results).print_text(false, 20)
|
||||
File.write(ENV['FILENAME'] + "." + profile_type.to_s, Marshal.dump(results)) if ENV['FILENAME']
|
||||
end
|
||||
|
||||
@@ -28,6 +28,6 @@ class CommentForm < Liquid::Block
|
||||
end
|
||||
|
||||
def wrap_in_form(article, input)
|
||||
%Q{<form id="article-#{article.id}-comment-form" class="comment-form" method="post" action="">\n#{input}\n</form>}
|
||||
%(<form id="article-#{article.id}-comment-form" class="comment-form" method="post" action="">\n#{input}\n</form>)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,7 +5,7 @@ module Database
|
||||
# to liquid as assigns. All this is based on Shopify
|
||||
def self.tables
|
||||
@tables ||= begin
|
||||
db = YAML.load_file(File.dirname(__FILE__) + '/vision.database.yml')
|
||||
db = YAML.load_file("#{__dir__}/vision.database.yml")
|
||||
|
||||
# From vision source
|
||||
db['products'].each do |product|
|
||||
@@ -39,7 +39,7 @@ module Database
|
||||
end
|
||||
end
|
||||
|
||||
if __FILE__ == $0
|
||||
if __FILE__ == $PROGRAM_NAME
|
||||
p Database.tables['collections']['frontpage'].keys
|
||||
#p Database.tables['blog']['articles']
|
||||
# p Database.tables['blog']['articles']
|
||||
end
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
require 'json'
|
||||
|
||||
module JsonFilter
|
||||
|
||||
def json(object)
|
||||
JSON.dump(object.reject {|k,v| k == "collections" })
|
||||
JSON.dump(object.reject { |k, v| k == "collections" })
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
$:.unshift File.dirname(__FILE__) + '/../../lib'
|
||||
require File.dirname(__FILE__) + '/../../lib/liquid'
|
||||
$:.unshift __dir__ + '/../../lib'
|
||||
require_relative '../../lib/liquid'
|
||||
|
||||
require File.dirname(__FILE__) + '/comment_form'
|
||||
require File.dirname(__FILE__) + '/paginate'
|
||||
require File.dirname(__FILE__) + '/json_filter'
|
||||
require File.dirname(__FILE__) + '/money_filter'
|
||||
require File.dirname(__FILE__) + '/shop_filter'
|
||||
require File.dirname(__FILE__) + '/tag_filter'
|
||||
require File.dirname(__FILE__) + '/weight_filter'
|
||||
require_relative 'comment_form'
|
||||
require_relative 'paginate'
|
||||
require_relative 'json_filter'
|
||||
require_relative 'money_filter'
|
||||
require_relative 'shop_filter'
|
||||
require_relative 'tag_filter'
|
||||
require_relative 'weight_filter'
|
||||
|
||||
Liquid::Template.register_tag 'paginate', Paginate
|
||||
Liquid::Template.register_tag 'form', CommentForm
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
module MoneyFilter
|
||||
|
||||
def money_with_currency(money)
|
||||
return '' if money.nil?
|
||||
sprintf("$ %.2f USD", money/100.0)
|
||||
sprintf("$ %.2f USD", money / 100.0)
|
||||
end
|
||||
|
||||
def money(money)
|
||||
return '' if money.nil?
|
||||
sprintf("$ %.2f", money/100.0)
|
||||
sprintf("$ %.2f", money / 100.0)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -4,8 +4,6 @@ class Paginate < Liquid::Block
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
|
||||
@nodelist = []
|
||||
|
||||
if markup =~ Syntax
|
||||
@collection_name = $1
|
||||
@page_size = if $2
|
||||
@@ -44,23 +42,22 @@ class Paginate < Liquid::Block
|
||||
page_count = (collection_size.to_f / @page_size.to_f).to_f.ceil + 1
|
||||
|
||||
pagination['items'] = collection_size
|
||||
pagination['pages'] = page_count -1
|
||||
pagination['previous'] = link('« Previous', current_page-1 ) unless 1 >= current_page
|
||||
pagination['next'] = link('Next »', current_page+1 ) unless page_count <= current_page+1
|
||||
pagination['pages'] = page_count - 1
|
||||
pagination['previous'] = link('« Previous', current_page - 1) unless 1 >= current_page
|
||||
pagination['next'] = link('Next »', current_page + 1) unless page_count <= current_page + 1
|
||||
pagination['parts'] = []
|
||||
|
||||
hellip_break = false
|
||||
|
||||
if page_count > 2
|
||||
1.upto(page_count-1) do |page|
|
||||
|
||||
1.upto(page_count - 1) do |page|
|
||||
if current_page == page
|
||||
pagination['parts'] << no_link(page)
|
||||
elsif page == 1
|
||||
pagination['parts'] << link(page, page)
|
||||
elsif page == page_count -1
|
||||
elsif page == page_count - 1
|
||||
pagination['parts'] << link(page, page)
|
||||
elsif page <= current_page - @attributes['window_size'] or page >= current_page + @attributes['window_size']
|
||||
elsif page <= current_page - @attributes['window_size'] || page >= current_page + @attributes['window_size']
|
||||
next if hellip_break
|
||||
pagination['parts'] << no_link('…')
|
||||
hellip_break = true
|
||||
@@ -73,18 +70,18 @@ class Paginate < Liquid::Block
|
||||
end
|
||||
end
|
||||
|
||||
render_all(@nodelist, context)
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def no_link(title)
|
||||
{ 'title' => title, 'is_link' => false}
|
||||
{ 'title' => title, 'is_link' => false }
|
||||
end
|
||||
|
||||
def link(title, page)
|
||||
{ 'title' => title, 'url' => current_url + "?page=#{page}", 'is_link' => true}
|
||||
{ 'title' => title, 'url' => current_url + "?page=#{page}", 'is_link' => true }
|
||||
end
|
||||
|
||||
def current_url
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
module ShopFilter
|
||||
|
||||
def asset_url(input)
|
||||
"/files/1/[shop_id]/[shop_id]/assets/#{input}"
|
||||
end
|
||||
@@ -16,16 +15,16 @@ module ShopFilter
|
||||
%(<script src="#{url}" type="text/javascript"></script>)
|
||||
end
|
||||
|
||||
def stylesheet_tag(url, media="all")
|
||||
def stylesheet_tag(url, media = "all")
|
||||
%(<link href="#{url}" rel="stylesheet" type="text/css" media="#{media}" />)
|
||||
end
|
||||
|
||||
def link_to(link, url, title="")
|
||||
%|<a href="#{url}" title="#{title}">#{link}</a>|
|
||||
def link_to(link, url, title = "")
|
||||
%(<a href="#{url}" title="#{title}">#{link}</a>)
|
||||
end
|
||||
|
||||
def img_tag(url, alt="")
|
||||
%|<img src="#{url}" alt="#{alt}" />|
|
||||
def img_tag(url, alt = "")
|
||||
%(<img src="#{url}" alt="#{alt}" />)
|
||||
end
|
||||
|
||||
def link_to_vendor(vendor)
|
||||
@@ -53,7 +52,6 @@ module ShopFilter
|
||||
end
|
||||
|
||||
def product_img_url(url, style = 'small')
|
||||
|
||||
unless url =~ /\Aproducts\/([\w\-\_]+)\.(\w{2,4})/
|
||||
raise ArgumentError, 'filter "size" can only be called on product images'
|
||||
end
|
||||
@@ -69,7 +67,6 @@ module ShopFilter
|
||||
end
|
||||
|
||||
def default_pagination(paginate)
|
||||
|
||||
html = []
|
||||
html << %(<span class="prev">#{link_to(paginate['previous']['title'], paginate['previous']['url'])}</span>) if paginate['previous']
|
||||
|
||||
@@ -106,5 +103,4 @@ module ShopFilter
|
||||
result.gsub!(/\A-+/, '') if result[0] == '-'
|
||||
result
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
module TagFilter
|
||||
|
||||
def link_to_tag(label, tag)
|
||||
"<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tag}\">#{label}</a>"
|
||||
end
|
||||
|
||||
def highlight_active_tag(tag, css_class='active')
|
||||
def highlight_active_tag(tag, css_class = 'active')
|
||||
if @context['current_tags'].include?(tag)
|
||||
"<span class=\"#{css_class}\">#{tag}</span>"
|
||||
else
|
||||
@@ -21,5 +20,4 @@ module TagFilter
|
||||
tags = (@context['current_tags'] - [tag]).uniq
|
||||
"<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tags.join("+")}\">#{label}</a>"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
module WeightFilter
|
||||
|
||||
def weight(grams)
|
||||
sprintf("%.2f", grams / 1000)
|
||||
end
|
||||
@@ -7,5 +6,4 @@ module WeightFilter
|
||||
def weight_with_unit(grams)
|
||||
"#{weight(grams)} kg"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -6,18 +6,17 @@
|
||||
# Shopify which is likely the biggest user of liquid in the world which something to the tune of several
|
||||
# million Template#render calls a day.
|
||||
|
||||
require File.dirname(__FILE__) + '/shopify/liquid'
|
||||
require File.dirname(__FILE__) + '/shopify/database.rb'
|
||||
require_relative 'shopify/liquid'
|
||||
require_relative 'shopify/database'
|
||||
|
||||
class ThemeRunner
|
||||
class FileSystem
|
||||
|
||||
def initialize(path)
|
||||
@path = path
|
||||
end
|
||||
|
||||
# Called by Liquid to retrieve a template file
|
||||
def read_template_file(template_path, context)
|
||||
def read_template_file(template_path)
|
||||
File.read(@path + '/' + template_path + '.liquid')
|
||||
end
|
||||
end
|
||||
@@ -25,7 +24,7 @@ class ThemeRunner
|
||||
# Load all templates into memory, do this now so that
|
||||
# we don't profile IO.
|
||||
def initialize
|
||||
@tests = Dir[File.dirname(__FILE__) + '/tests/**/*.liquid'].collect do |test|
|
||||
@tests = Dir[__dir__ + '/tests/**/*.liquid'].collect do |test|
|
||||
next if File.basename(test) == 'theme.liquid'
|
||||
|
||||
theme_path = File.dirname(test) + '/theme.liquid'
|
||||
@@ -38,7 +37,6 @@ class ThemeRunner
|
||||
# Dup assigns because will make some changes to them
|
||||
|
||||
@tests.each do |liquid, layout, template_name|
|
||||
|
||||
tmpl = Liquid::Template.new
|
||||
tmpl.parse(liquid)
|
||||
tmpl = Liquid::Template.new
|
||||
@@ -46,20 +44,17 @@ class ThemeRunner
|
||||
end
|
||||
end
|
||||
|
||||
def run
|
||||
def run
|
||||
# Dup assigns because will make some changes to them
|
||||
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)
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def compile_and_render(template, layout, assigns, page_template, template_file)
|
||||
tmpl = Liquid::Template.new
|
||||
tmpl.assigns['page_title'] = 'Page title'
|
||||
|
||||
@@ -1,27 +1,48 @@
|
||||
require 'test_helper'
|
||||
|
||||
class AssignTest < Test::Unit::TestCase
|
||||
class AssignTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_assign_with_hyphen_in_variable_name
|
||||
template_source = <<-END_TEMPLATE
|
||||
{% assign this-thing = 'Print this-thing' %}
|
||||
{{ this-thing }}
|
||||
END_TEMPLATE
|
||||
template = Template.parse(template_source)
|
||||
rendered = template.render!
|
||||
assert_equal "Print this-thing", rendered.strip
|
||||
end
|
||||
|
||||
def test_assigned_variable
|
||||
assert_template_result('.foo.',
|
||||
'{% assign foo = values %}.{{ foo[0] }}.',
|
||||
'values' => %w{foo bar baz})
|
||||
'{% assign foo = values %}.{{ foo[0] }}.',
|
||||
'values' => %w(foo bar baz))
|
||||
|
||||
assert_template_result('.bar.',
|
||||
'{% assign foo = values %}.{{ foo[1] }}.',
|
||||
'values' => %w{foo bar baz})
|
||||
'{% assign foo = values %}.{{ foo[1] }}.',
|
||||
'values' => %w(foo bar baz))
|
||||
end
|
||||
|
||||
def test_assign_with_filter
|
||||
assert_template_result('.bar.',
|
||||
'{% assign foo = values | split: "," %}.{{ foo[1] }}.',
|
||||
'values' => "foo,bar,baz")
|
||||
'{% assign foo = values | split: "," %}.{{ foo[1] }}.',
|
||||
'values' => "foo,bar,baz")
|
||||
end
|
||||
|
||||
def test_assign_syntax_error
|
||||
assert_match_syntax_error(/assign/,
|
||||
'{% assign foo not values %}.',
|
||||
'values' => "foo,bar,baz")
|
||||
'{% assign foo not values %}.',
|
||||
'values' => "foo,bar,baz")
|
||||
end
|
||||
|
||||
def test_assign_uses_error_mode
|
||||
with_error_mode(:strict) do
|
||||
assert_raises(SyntaxError) do
|
||||
Template.parse("{% assign foo = ('X' | downcase) %}")
|
||||
end
|
||||
end
|
||||
with_error_mode(:lax) do
|
||||
assert Template.parse("{% assign foo = ('X' | downcase) %}")
|
||||
end
|
||||
end
|
||||
end # AssignTest
|
||||
|
||||
@@ -9,12 +9,12 @@ class FoobarTag < Liquid::Tag
|
||||
end
|
||||
|
||||
class BlankTestFileSystem
|
||||
def read_template_file(template_path, context)
|
||||
def read_template_file(template_path)
|
||||
template_path
|
||||
end
|
||||
end
|
||||
|
||||
class BlankTest < Test::Unit::TestCase
|
||||
class BlankTest < Minitest::Test
|
||||
include Liquid
|
||||
N = 10
|
||||
|
||||
@@ -31,7 +31,7 @@ class BlankTest < Test::Unit::TestCase
|
||||
end
|
||||
|
||||
def test_new_tags_are_not_blank_by_default
|
||||
assert_template_result(" "*N, wrap_in_for("{% foobar %}"))
|
||||
assert_template_result(" " * N, wrap_in_for("{% foobar %}"))
|
||||
end
|
||||
|
||||
def test_loops_are_blank
|
||||
@@ -47,7 +47,7 @@ class BlankTest < Test::Unit::TestCase
|
||||
end
|
||||
|
||||
def test_mark_as_blank_only_during_parsing
|
||||
assert_template_result(" "*(N+1), wrap(" {% if false %} this never happens, but still, this block is not blank {% endif %}"))
|
||||
assert_template_result(" " * (N + 1), wrap(" {% if false %} this never happens, but still, this block is not blank {% endif %}"))
|
||||
end
|
||||
|
||||
def test_comments_are_blank
|
||||
@@ -60,9 +60,9 @@ class BlankTest < Test::Unit::TestCase
|
||||
|
||||
def test_nested_blocks_are_blank_but_only_if_all_children_are
|
||||
assert_template_result("", wrap(wrap(" ")))
|
||||
assert_template_result("\n but this is not "*(N+1),
|
||||
wrap(%q{{% if true %} {% comment %} this is blank {% endcomment %} {% endif %}
|
||||
{% if true %} but this is not {% endif %}}))
|
||||
assert_template_result("\n but this is not " * (N + 1),
|
||||
wrap('{% if true %} {% comment %} this is blank {% endcomment %} {% endif %}
|
||||
{% if true %} but this is not {% endif %}'))
|
||||
end
|
||||
|
||||
def test_assigns_are_blank
|
||||
@@ -76,31 +76,31 @@ class BlankTest < Test::Unit::TestCase
|
||||
|
||||
def test_whitespace_is_not_blank_if_other_stuff_is_present
|
||||
body = " x "
|
||||
assert_template_result(body*(N+1), wrap(body))
|
||||
assert_template_result(body * (N + 1), wrap(body))
|
||||
end
|
||||
|
||||
def test_increment_is_not_blank
|
||||
assert_template_result(" 0"*2*(N+1), wrap("{% assign foo = 0 %} {% increment foo %} {% decrement foo %}"))
|
||||
assert_template_result(" 0" * 2 * (N + 1), wrap("{% assign foo = 0 %} {% increment foo %} {% decrement foo %}"))
|
||||
end
|
||||
|
||||
def test_cycle_is_not_blank
|
||||
assert_template_result(" "*((N+1)/2)+" ", wrap("{% cycle ' ', ' ' %}"))
|
||||
assert_template_result(" " * ((N + 1) / 2) + " ", wrap("{% cycle ' ', ' ' %}"))
|
||||
end
|
||||
|
||||
def test_raw_is_not_blank
|
||||
assert_template_result(" "*(N+1), wrap(" {% raw %} {% endraw %}"))
|
||||
assert_template_result(" " * (N + 1), wrap(" {% raw %} {% endraw %}"))
|
||||
end
|
||||
|
||||
def test_include_is_blank
|
||||
Liquid::Template.file_system = BlankTestFileSystem.new
|
||||
assert_template_result "foobar"*(N+1), wrap("{% include 'foobar' %}")
|
||||
assert_template_result " foobar "*(N+1), wrap("{% include ' foobar ' %}")
|
||||
assert_template_result " "*(N+1), wrap(" {% include ' ' %} ")
|
||||
assert_template_result "foobar" * (N + 1), wrap("{% include 'foobar' %}")
|
||||
assert_template_result " foobar " * (N + 1), wrap("{% include ' foobar ' %}")
|
||||
assert_template_result " " * (N + 1), wrap(" {% include ' ' %} ")
|
||||
end
|
||||
|
||||
def test_case_is_blank
|
||||
assert_template_result("", wrap(" {% assign foo = 'bar' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} {% endcase %} "))
|
||||
assert_template_result("", wrap(" {% assign foo = 'else' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} {% endcase %} "))
|
||||
assert_template_result(" x "*(N+1), wrap(" {% assign foo = 'else' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} x {% endcase %} "))
|
||||
assert_template_result(" x " * (N + 1), wrap(" {% assign foo = 'else' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} x {% endcase %} "))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
require 'test_helper'
|
||||
|
||||
class CaptureTest < Test::Unit::TestCase
|
||||
class CaptureTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_captures_block_content_in_variable
|
||||
assert_template_result("test string", "{% capture 'var' %}test string{% endcapture %}{{var}}", {})
|
||||
end
|
||||
|
||||
def test_capture_with_hyphen_in_variable_name
|
||||
template_source = <<-END_TEMPLATE
|
||||
{% capture this-thing %}Print this-thing{% endcapture %}
|
||||
{{ this-thing }}
|
||||
END_TEMPLATE
|
||||
template = Template.parse(template_source)
|
||||
rendered = template.render!
|
||||
assert_equal "Print this-thing", rendered.strip
|
||||
end
|
||||
|
||||
def test_capture_to_variable_from_outer_scope_if_existing
|
||||
template_source = <<-END_TEMPLATE
|
||||
{% assign var = '' %}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
require 'test_helper'
|
||||
|
||||
class ContextTest < Test::Unit::TestCase
|
||||
class ContextTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_override_global_filter
|
||||
@@ -16,18 +16,17 @@ class ContextTest < Test::Unit::TestCase
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_filter(global)
|
||||
assert_equal 'Global test', Template.parse("{{'test' | notice }}").render!
|
||||
assert_equal 'Local test', Template.parse("{{'test' | notice }}").render!({}, :filters => [local])
|
||||
with_global_filter(global) do
|
||||
assert_equal 'Global test', Template.parse("{{'test' | notice }}").render!
|
||||
assert_equal 'Local test', Template.parse("{{'test' | notice }}").render!({}, filters: [local])
|
||||
end
|
||||
end
|
||||
|
||||
def test_has_key_will_not_add_an_error_for_missing_keys
|
||||
Template.error_mode = :strict
|
||||
|
||||
context = Context.new
|
||||
|
||||
context.has_key?('unknown')
|
||||
|
||||
assert_empty context.errors
|
||||
with_error_mode :strict do
|
||||
context = Context.new
|
||||
context.key?('unknown')
|
||||
assert_empty context.errors
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
19
test/integration/document_test.rb
Normal file
19
test/integration/document_test.rb
Normal file
@@ -0,0 +1,19 @@
|
||||
require 'test_helper'
|
||||
|
||||
class DocumentTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_unexpected_outer_tag
|
||||
exc = assert_raises(SyntaxError) do
|
||||
Template.parse("{% else %}")
|
||||
end
|
||||
assert_equal exc.message, "Liquid syntax error: Unexpected outer 'else' tag"
|
||||
end
|
||||
|
||||
def test_unknown_tag
|
||||
exc = assert_raises(SyntaxError) do
|
||||
Template.parse("{% foo %}")
|
||||
end
|
||||
assert_equal exc.message, "Liquid syntax error: Unknown tag 'foo'"
|
||||
end
|
||||
end
|
||||
@@ -13,13 +13,12 @@ class ContextDrop < Liquid::Drop
|
||||
@context['forloop.index']
|
||||
end
|
||||
|
||||
def before_method(method)
|
||||
return @context[method]
|
||||
def liquid_method_missing(method)
|
||||
@context[method]
|
||||
end
|
||||
end
|
||||
|
||||
class ProductDrop < Liquid::Drop
|
||||
|
||||
class TextDrop < Liquid::Drop
|
||||
def array
|
||||
['text1', 'text2']
|
||||
@@ -31,8 +30,8 @@ class ProductDrop < Liquid::Drop
|
||||
end
|
||||
|
||||
class CatchallDrop < Liquid::Drop
|
||||
def before_method(method)
|
||||
return 'method: ' << method.to_s
|
||||
def liquid_method_missing(method)
|
||||
'catchall_method: ' << method.to_s
|
||||
end
|
||||
end
|
||||
|
||||
@@ -48,14 +47,19 @@ class ProductDrop < Liquid::Drop
|
||||
ContextDrop.new
|
||||
end
|
||||
|
||||
def user_input
|
||||
"foo".taint
|
||||
end
|
||||
|
||||
protected
|
||||
def callmenot
|
||||
"protected"
|
||||
end
|
||||
|
||||
def callmenot
|
||||
"protected"
|
||||
end
|
||||
end
|
||||
|
||||
class EnumerableDrop < Liquid::Drop
|
||||
def before_method(method)
|
||||
def liquid_method_missing(method)
|
||||
method
|
||||
end
|
||||
|
||||
@@ -89,7 +93,7 @@ end
|
||||
class RealEnumerableDrop < Liquid::Drop
|
||||
include Enumerable
|
||||
|
||||
def before_method(method)
|
||||
def liquid_method_missing(method)
|
||||
method
|
||||
end
|
||||
|
||||
@@ -100,12 +104,36 @@ class RealEnumerableDrop < Liquid::Drop
|
||||
end
|
||||
end
|
||||
|
||||
class DropsTest < Test::Unit::TestCase
|
||||
class DropsTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_product_drop
|
||||
assert_nothing_raised do
|
||||
tpl = Liquid::Template.parse( ' ' )
|
||||
tpl = Liquid::Template.parse(' ')
|
||||
assert_equal ' ', tpl.render!('product' => ProductDrop.new)
|
||||
end
|
||||
|
||||
def test_rendering_raises_on_tainted_attr
|
||||
with_taint_mode(:error) do
|
||||
tpl = Liquid::Template.parse('{{ product.user_input }}')
|
||||
assert_raises TaintedError do
|
||||
tpl.render!('product' => ProductDrop.new)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_rendering_warns_on_tainted_attr
|
||||
with_taint_mode(:warn) do
|
||||
tpl = Liquid::Template.parse('{{ product.user_input }}')
|
||||
context = Context.new('product' => ProductDrop.new)
|
||||
tpl.render!(context)
|
||||
assert_equal [Liquid::TaintedError], context.warnings.map(&:class)
|
||||
assert_equal "variable 'product.user_input' is tainted and was not escaped", context.warnings.first.to_s(false)
|
||||
end
|
||||
end
|
||||
|
||||
def test_rendering_doesnt_raise_on_escaped_tainted_attr
|
||||
with_taint_mode(:error) do
|
||||
tpl = Liquid::Template.parse('{{ product.user_input | escape }}')
|
||||
tpl.render!('product' => ProductDrop.new)
|
||||
end
|
||||
end
|
||||
@@ -125,37 +153,37 @@ class DropsTest < Test::Unit::TestCase
|
||||
end
|
||||
|
||||
def test_text_drop
|
||||
output = Liquid::Template.parse( ' {{ product.texts.text }} ' ).render!('product' => ProductDrop.new)
|
||||
output = Liquid::Template.parse(' {{ product.texts.text }} ').render!('product' => ProductDrop.new)
|
||||
assert_equal ' text1 ', output
|
||||
end
|
||||
|
||||
def test_unknown_method
|
||||
output = Liquid::Template.parse( ' {{ product.catchall.unknown }} ' ).render!('product' => ProductDrop.new)
|
||||
assert_equal ' method: unknown ', output
|
||||
def test_catchall_unknown_method
|
||||
output = Liquid::Template.parse(' {{ product.catchall.unknown }} ').render!('product' => ProductDrop.new)
|
||||
assert_equal ' catchall_method: unknown ', output
|
||||
end
|
||||
|
||||
def test_integer_argument_drop
|
||||
output = Liquid::Template.parse( ' {{ product.catchall[8] }} ' ).render!('product' => ProductDrop.new)
|
||||
assert_equal ' method: 8 ', output
|
||||
def test_catchall_integer_argument_drop
|
||||
output = Liquid::Template.parse(' {{ product.catchall[8] }} ').render!('product' => ProductDrop.new)
|
||||
assert_equal ' catchall_method: 8 ', output
|
||||
end
|
||||
|
||||
def test_text_array_drop
|
||||
output = Liquid::Template.parse( '{% for text in product.texts.array %} {{text}} {% endfor %}' ).render!('product' => ProductDrop.new)
|
||||
output = Liquid::Template.parse('{% for text in product.texts.array %} {{text}} {% endfor %}').render!('product' => ProductDrop.new)
|
||||
assert_equal ' text1 text2 ', output
|
||||
end
|
||||
|
||||
def test_context_drop
|
||||
output = Liquid::Template.parse( ' {{ context.bar }} ' ).render!('context' => ContextDrop.new, 'bar' => "carrot")
|
||||
output = Liquid::Template.parse(' {{ context.bar }} ').render!('context' => ContextDrop.new, 'bar' => "carrot")
|
||||
assert_equal ' carrot ', output
|
||||
end
|
||||
|
||||
def test_nested_context_drop
|
||||
output = Liquid::Template.parse( ' {{ product.context.foo }} ' ).render!('product' => ProductDrop.new, 'foo' => "monkey")
|
||||
output = Liquid::Template.parse(' {{ product.context.foo }} ').render!('product' => ProductDrop.new, 'foo' => "monkey")
|
||||
assert_equal ' monkey ', output
|
||||
end
|
||||
|
||||
def test_protected
|
||||
output = Liquid::Template.parse( ' {{ product.callmenot }} ' ).render!('product' => ProductDrop.new)
|
||||
output = Liquid::Template.parse(' {{ product.callmenot }} ').render!('product' => ProductDrop.new)
|
||||
assert_equal ' ', output
|
||||
end
|
||||
|
||||
@@ -167,43 +195,43 @@ class DropsTest < Test::Unit::TestCase
|
||||
end
|
||||
|
||||
def test_scope
|
||||
assert_equal '1', Liquid::Template.parse( '{{ context.scopes }}' ).render!('context' => ContextDrop.new)
|
||||
assert_equal '2', Liquid::Template.parse( '{%for i in dummy%}{{ context.scopes }}{%endfor%}' ).render!('context' => ContextDrop.new, 'dummy' => [1])
|
||||
assert_equal '3', Liquid::Template.parse( '{%for i in dummy%}{%for i in dummy%}{{ context.scopes }}{%endfor%}{%endfor%}' ).render!('context' => ContextDrop.new, 'dummy' => [1])
|
||||
assert_equal '1', Liquid::Template.parse('{{ context.scopes }}').render!('context' => ContextDrop.new)
|
||||
assert_equal '2', Liquid::Template.parse('{%for i in dummy%}{{ context.scopes }}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1])
|
||||
assert_equal '3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ context.scopes }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1])
|
||||
end
|
||||
|
||||
def test_scope_though_proc
|
||||
assert_equal '1', Liquid::Template.parse( '{{ s }}' ).render!('context' => ContextDrop.new, 's' => Proc.new{|c| c['context.scopes'] })
|
||||
assert_equal '2', Liquid::Template.parse( '{%for i in dummy%}{{ s }}{%endfor%}' ).render!('context' => ContextDrop.new, 's' => Proc.new{|c| c['context.scopes'] }, 'dummy' => [1])
|
||||
assert_equal '3', Liquid::Template.parse( '{%for i in dummy%}{%for i in dummy%}{{ s }}{%endfor%}{%endfor%}' ).render!('context' => ContextDrop.new, 's' => Proc.new{|c| c['context.scopes'] }, 'dummy' => [1])
|
||||
assert_equal '1', Liquid::Template.parse('{{ s }}').render!('context' => ContextDrop.new, 's' => proc{ |c| c['context.scopes'] })
|
||||
assert_equal '2', Liquid::Template.parse('{%for i in dummy%}{{ s }}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc{ |c| c['context.scopes'] }, 'dummy' => [1])
|
||||
assert_equal '3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ s }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc{ |c| c['context.scopes'] }, 'dummy' => [1])
|
||||
end
|
||||
|
||||
def test_scope_with_assigns
|
||||
assert_equal 'variable', Liquid::Template.parse( '{% assign a = "variable"%}{{a}}' ).render!('context' => ContextDrop.new)
|
||||
assert_equal 'variable', Liquid::Template.parse( '{% assign a = "variable"%}{%for i in dummy%}{{a}}{%endfor%}' ).render!('context' => ContextDrop.new, 'dummy' => [1])
|
||||
assert_equal 'test', Liquid::Template.parse( '{% assign header_gif = "test"%}{{header_gif}}' ).render!('context' => ContextDrop.new)
|
||||
assert_equal 'test', Liquid::Template.parse( "{% assign header_gif = 'test'%}{{header_gif}}" ).render!('context' => ContextDrop.new)
|
||||
assert_equal 'variable', Liquid::Template.parse('{% assign a = "variable"%}{{a}}').render!('context' => ContextDrop.new)
|
||||
assert_equal 'variable', Liquid::Template.parse('{% assign a = "variable"%}{%for i in dummy%}{{a}}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1])
|
||||
assert_equal 'test', Liquid::Template.parse('{% assign header_gif = "test"%}{{header_gif}}').render!('context' => ContextDrop.new)
|
||||
assert_equal 'test', Liquid::Template.parse("{% assign header_gif = 'test'%}{{header_gif}}").render!('context' => ContextDrop.new)
|
||||
end
|
||||
|
||||
def test_scope_from_tags
|
||||
assert_equal '1', Liquid::Template.parse( '{% for i in context.scopes_as_array %}{{i}}{% endfor %}' ).render!('context' => ContextDrop.new, 'dummy' => [1])
|
||||
assert_equal '12', Liquid::Template.parse( '{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}' ).render!('context' => ContextDrop.new, 'dummy' => [1])
|
||||
assert_equal '123', Liquid::Template.parse( '{%for a in dummy%}{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}{% endfor %}' ).render!('context' => ContextDrop.new, 'dummy' => [1])
|
||||
assert_equal '1', Liquid::Template.parse('{% for i in context.scopes_as_array %}{{i}}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1])
|
||||
assert_equal '12', Liquid::Template.parse('{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1])
|
||||
assert_equal '123', Liquid::Template.parse('{%for a in dummy%}{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1])
|
||||
end
|
||||
|
||||
def test_access_context_from_drop
|
||||
assert_equal '123', Liquid::Template.parse( '{%for a in dummy%}{{ context.loop_pos }}{% endfor %}' ).render!('context' => ContextDrop.new, 'dummy' => [1,2,3])
|
||||
assert_equal '123', Liquid::Template.parse('{%for a in dummy%}{{ context.loop_pos }}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1, 2, 3])
|
||||
end
|
||||
|
||||
def test_enumerable_drop
|
||||
assert_equal '123', Liquid::Template.parse( '{% for c in collection %}{{c}}{% endfor %}').render!('collection' => EnumerableDrop.new)
|
||||
assert_equal '123', Liquid::Template.parse('{% for c in collection %}{{c}}{% endfor %}').render!('collection' => EnumerableDrop.new)
|
||||
end
|
||||
|
||||
def test_enumerable_drop_size
|
||||
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
|
||||
|
||||
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|
|
||||
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)
|
||||
|
||||
@@ -1,110 +1,226 @@
|
||||
require 'test_helper'
|
||||
|
||||
class ErrorDrop < Liquid::Drop
|
||||
def standard_error
|
||||
raise Liquid::StandardError, 'standard error'
|
||||
end
|
||||
|
||||
def argument_error
|
||||
raise Liquid::ArgumentError, 'argument error'
|
||||
end
|
||||
|
||||
def syntax_error
|
||||
raise Liquid::SyntaxError, 'syntax error'
|
||||
end
|
||||
|
||||
def exception
|
||||
raise Exception, 'exception'
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
class ErrorHandlingTest < Test::Unit::TestCase
|
||||
class ErrorHandlingTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_standard_error
|
||||
assert_nothing_raised do
|
||||
template = Liquid::Template.parse( ' {{ errors.standard_error }} ' )
|
||||
assert_equal ' Liquid error: standard error ', template.render('errors' => ErrorDrop.new)
|
||||
def test_templates_parsed_with_line_numbers_renders_them_in_errors
|
||||
template = <<-LIQUID
|
||||
Hello,
|
||||
|
||||
assert_equal 1, template.errors.size
|
||||
assert_equal StandardError, template.errors.first.class
|
||||
end
|
||||
{{ errors.standard_error }} will raise a standard error.
|
||||
|
||||
Bla bla test.
|
||||
|
||||
{{ errors.syntax_error }} will raise a syntax error.
|
||||
|
||||
This is an argument error: {{ errors.argument_error }}
|
||||
|
||||
Bla.
|
||||
LIQUID
|
||||
|
||||
expected = <<-TEXT
|
||||
Hello,
|
||||
|
||||
Liquid error (line 3): standard error will raise a standard error.
|
||||
|
||||
Bla bla test.
|
||||
|
||||
Liquid syntax error (line 7): syntax error will raise a syntax error.
|
||||
|
||||
This is an argument error: Liquid error (line 9): argument error
|
||||
|
||||
Bla.
|
||||
TEXT
|
||||
|
||||
output = Liquid::Template.parse(template, line_numbers: true).render('errors' => ErrorDrop.new)
|
||||
assert_equal expected, output
|
||||
end
|
||||
|
||||
def test_standard_error
|
||||
template = Liquid::Template.parse(' {{ errors.standard_error }} ')
|
||||
assert_equal ' Liquid error: standard error ', template.render('errors' => ErrorDrop.new)
|
||||
|
||||
assert_equal 1, template.errors.size
|
||||
assert_equal StandardError, template.errors.first.class
|
||||
end
|
||||
|
||||
def test_syntax
|
||||
template = Liquid::Template.parse(' {{ errors.syntax_error }} ')
|
||||
assert_equal ' Liquid syntax error: syntax error ', template.render('errors' => ErrorDrop.new)
|
||||
|
||||
assert_nothing_raised do
|
||||
|
||||
template = Liquid::Template.parse( ' {{ errors.syntax_error }} ' )
|
||||
assert_equal ' Liquid syntax error: syntax error ', template.render('errors' => ErrorDrop.new)
|
||||
|
||||
assert_equal 1, template.errors.size
|
||||
assert_equal SyntaxError, template.errors.first.class
|
||||
|
||||
end
|
||||
assert_equal 1, template.errors.size
|
||||
assert_equal SyntaxError, template.errors.first.class
|
||||
end
|
||||
|
||||
def test_argument
|
||||
assert_nothing_raised do
|
||||
template = Liquid::Template.parse(' {{ errors.argument_error }} ')
|
||||
assert_equal ' Liquid error: argument error ', template.render('errors' => ErrorDrop.new)
|
||||
|
||||
template = Liquid::Template.parse( ' {{ errors.argument_error }} ' )
|
||||
assert_equal ' Liquid error: argument error ', template.render('errors' => ErrorDrop.new)
|
||||
|
||||
assert_equal 1, template.errors.size
|
||||
assert_equal ArgumentError, template.errors.first.class
|
||||
end
|
||||
assert_equal 1, template.errors.size
|
||||
assert_equal ArgumentError, template.errors.first.class
|
||||
end
|
||||
|
||||
def test_missing_endtag_parse_time_error
|
||||
assert_raise(Liquid::SyntaxError) do
|
||||
assert_raises(Liquid::SyntaxError) do
|
||||
Liquid::Template.parse(' {% for a in b %} ... ')
|
||||
end
|
||||
end
|
||||
|
||||
def test_unrecognized_operator
|
||||
with_error_mode(:strict) do
|
||||
assert_raise(SyntaxError) do
|
||||
assert_raises(SyntaxError) do
|
||||
Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def test_lax_unrecognized_operator
|
||||
assert_nothing_raised do
|
||||
template = Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', :error_mode => :lax)
|
||||
assert_equal ' Liquid error: Unknown operator =! ', template.render
|
||||
assert_equal 1, template.errors.size
|
||||
assert_equal Liquid::ArgumentError, template.errors.first.class
|
||||
template = Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', error_mode: :lax)
|
||||
assert_equal ' Liquid error: Unknown operator =! ', template.render
|
||||
assert_equal 1, template.errors.size
|
||||
assert_equal Liquid::ArgumentError, template.errors.first.class
|
||||
end
|
||||
|
||||
def test_with_line_numbers_adds_numbers_to_parser_errors
|
||||
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
|
||||
|
||||
def test_parsing_warn_with_line_numbers_adds_numbers_to_lexer_errors
|
||||
template = Liquid::Template.parse('
|
||||
foobar
|
||||
|
||||
{% if 1 =! 2 %}ok{% endif %}
|
||||
|
||||
bla
|
||||
',
|
||||
error_mode: :warn,
|
||||
line_numbers: true
|
||||
)
|
||||
|
||||
assert_equal ['Liquid syntax error (line 4): Unexpected character = in "1 =! 2"'],
|
||||
template.warnings.map(&:message)
|
||||
end
|
||||
|
||||
def test_parsing_strict_with_line_numbers_adds_numbers_to_lexer_errors
|
||||
err = assert_raises(SyntaxError) do
|
||||
Liquid::Template.parse('
|
||||
foobar
|
||||
|
||||
{% if 1 =! 2 %}ok{% endif %}
|
||||
|
||||
bla
|
||||
',
|
||||
error_mode: :strict,
|
||||
line_numbers: true
|
||||
)
|
||||
end
|
||||
|
||||
assert_equal 'Liquid syntax error (line 4): Unexpected character = in "1 =! 2"', err.message
|
||||
end
|
||||
|
||||
def test_syntax_errors_in_nested_blocks_have_correct_line_number
|
||||
err = assert_raises(SyntaxError) do
|
||||
Liquid::Template.parse('
|
||||
foobar
|
||||
|
||||
{% if 1 != 2 %}
|
||||
{% foo %}
|
||||
{% endif %}
|
||||
|
||||
bla
|
||||
',
|
||||
line_numbers: true
|
||||
)
|
||||
end
|
||||
|
||||
assert_equal "Liquid syntax error (line 5): Unknown tag 'foo'", err.message
|
||||
end
|
||||
|
||||
def test_strict_error_messages
|
||||
err = assert_raise(SyntaxError) do
|
||||
Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', :error_mode => :strict)
|
||||
err = assert_raises(SyntaxError) do
|
||||
Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', error_mode: :strict)
|
||||
end
|
||||
assert_equal 'Unexpected character = in "1 =! 2"', err.message
|
||||
assert_equal 'Liquid syntax error: Unexpected character = in "1 =! 2"', err.message
|
||||
|
||||
err = assert_raise(SyntaxError) do
|
||||
Liquid::Template.parse('{{%%%}}', :error_mode => :strict)
|
||||
err = assert_raises(SyntaxError) do
|
||||
Liquid::Template.parse('{{%%%}}', error_mode: :strict)
|
||||
end
|
||||
assert_equal 'Unexpected character % in "{{%%%}}"', err.message
|
||||
assert_equal 'Liquid syntax error: Unexpected character % in "{{%%%}}"', err.message
|
||||
end
|
||||
|
||||
def test_warnings
|
||||
template = Liquid::Template.parse('{% if ~~~ %}{{%%%}}{% else %}{{ hello. }}{% endif %}', :error_mode => :warn)
|
||||
template = Liquid::Template.parse('{% if ~~~ %}{{%%%}}{% else %}{{ hello. }}{% endif %}', error_mode: :warn)
|
||||
assert_equal 3, template.warnings.size
|
||||
assert_equal 'Unexpected character ~ in "~~~"', template.warnings[0].message
|
||||
assert_equal 'Unexpected character % in "{{%%%}}"', template.warnings[1].message
|
||||
assert_equal 'Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].message
|
||||
assert_equal 'Unexpected character ~ in "~~~"', template.warnings[0].to_s(false)
|
||||
assert_equal 'Unexpected character % in "{{%%%}}"', template.warnings[1].to_s(false)
|
||||
assert_equal 'Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].to_s(false)
|
||||
assert_equal '', template.render
|
||||
end
|
||||
|
||||
def test_warning_line_numbers
|
||||
template = Liquid::Template.parse("{% if ~~~ %}\n{{%%%}}{% else %}\n{{ hello. }}{% endif %}", error_mode: :warn, line_numbers: true)
|
||||
assert_equal 'Liquid syntax error (line 1): Unexpected character ~ in "~~~"', template.warnings[0].message
|
||||
assert_equal 'Liquid syntax error (line 2): Unexpected character % in "{{%%%}}"', template.warnings[1].message
|
||||
assert_equal 'Liquid syntax error (line 3): Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].message
|
||||
assert_equal 3, template.warnings.size
|
||||
assert_equal [1, 2, 3], template.warnings.map(&:line_number)
|
||||
end
|
||||
|
||||
# Liquid should not catch Exceptions that are not subclasses of StandardError, like Interrupt and NoMemoryError
|
||||
def test_exceptions_propagate
|
||||
assert_raise Exception do
|
||||
template = Liquid::Template.parse( ' {{ errors.exception }} ' )
|
||||
assert_raises Exception do
|
||||
template = Liquid::Template.parse('{{ errors.exception }}')
|
||||
template.render('errors' => ErrorDrop.new)
|
||||
end
|
||||
end
|
||||
end # ErrorHandlingTest
|
||||
|
||||
def test_exception_handler_with_string_result
|
||||
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)
|
||||
handler = ->(e) { e.is_a?(Liquid::Error) ? e : InternalError.new('internal') }
|
||||
output = template.render({ 'errors' => ErrorDrop.new }, exception_handler: handler)
|
||||
assert_equal 'This is a runtime error: Liquid error (line 1): internal', output
|
||||
assert_equal [InternalError], template.errors.map(&:class)
|
||||
end
|
||||
|
||||
class TestFileSystem
|
||||
def read_template_file(template_path)
|
||||
"{{ errors.argument_error }}"
|
||||
end
|
||||
end
|
||||
|
||||
def test_included_template_name_with_line_numbers
|
||||
old_file_system = Liquid::Template.file_system
|
||||
|
||||
begin
|
||||
Liquid::Template.file_system = TestFileSystem.new
|
||||
template = Liquid::Template.parse("Argument error:\n{% include 'product' %}", line_numbers: true)
|
||||
page = template.render('errors' => ErrorDrop.new)
|
||||
ensure
|
||||
Liquid::Template.file_system = old_file_system
|
||||
end
|
||||
assert_equal "Argument error:\nLiquid error (product line 1): argument error", page
|
||||
assert_equal "product", template.errors.first.template_name
|
||||
end
|
||||
end
|
||||
|
||||
@@ -17,14 +17,20 @@ module CanadianMoneyFilter
|
||||
end
|
||||
|
||||
module SubstituteFilter
|
||||
def substitute(input, params={})
|
||||
def substitute(input, params = {})
|
||||
input.gsub(/%\{(\w+)\}/) { |match| params[$1] }
|
||||
end
|
||||
end
|
||||
|
||||
class FiltersTest < Test::Unit::TestCase
|
||||
class FiltersTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
module OverrideObjectMethodFilter
|
||||
def tap(input)
|
||||
"tap overridden"
|
||||
end
|
||||
end
|
||||
|
||||
def setup
|
||||
@context = Context.new
|
||||
end
|
||||
@@ -33,13 +39,13 @@ class FiltersTest < Test::Unit::TestCase
|
||||
@context['var'] = 1000
|
||||
@context.add_filters(MoneyFilter)
|
||||
|
||||
assert_equal ' 1000$ ', Variable.new("var | money").render(@context)
|
||||
assert_equal ' 1000$ ', Template.parse("{{var | money}}").render(@context)
|
||||
end
|
||||
|
||||
def test_underscore_in_filter_name
|
||||
@context['var'] = 1000
|
||||
@context.add_filters(MoneyFilter)
|
||||
assert_equal ' 1000$ ', Variable.new("var | money_with_underscore").render(@context)
|
||||
assert_equal ' 1000$ ', Template.parse("{{var | money_with_underscore}}").render(@context)
|
||||
end
|
||||
|
||||
def test_second_filter_overwrites_first
|
||||
@@ -47,75 +53,115 @@ class FiltersTest < Test::Unit::TestCase
|
||||
@context.add_filters(MoneyFilter)
|
||||
@context.add_filters(CanadianMoneyFilter)
|
||||
|
||||
assert_equal ' 1000$ CAD ', Variable.new("var | money").render(@context)
|
||||
assert_equal ' 1000$ CAD ', Template.parse("{{var | money}}").render(@context)
|
||||
end
|
||||
|
||||
def test_size
|
||||
@context['var'] = 'abcd'
|
||||
@context.add_filters(MoneyFilter)
|
||||
|
||||
assert_equal 4, Variable.new("var | size").render(@context)
|
||||
assert_equal '4', Template.parse("{{var | size}}").render(@context)
|
||||
end
|
||||
|
||||
def test_join
|
||||
@context['var'] = [1,2,3,4]
|
||||
@context['var'] = [1, 2, 3, 4]
|
||||
|
||||
assert_equal "1 2 3 4", Variable.new("var | join").render(@context)
|
||||
assert_equal "1 2 3 4", Template.parse("{{var | join}}").render(@context)
|
||||
end
|
||||
|
||||
def test_sort
|
||||
@context['value'] = 3
|
||||
@context['numbers'] = [2,1,4,3]
|
||||
@context['numbers'] = [2, 1, 4, 3]
|
||||
@context['words'] = ['expected', 'as', 'alphabetic']
|
||||
@context['arrays'] = [['flattened'], ['are']]
|
||||
@context['arrays'] = ['flower', 'are']
|
||||
@context['case_sensitive'] = ['sensitive', 'Expected', 'case']
|
||||
|
||||
assert_equal [1,2,3,4], Variable.new("numbers | sort").render(@context)
|
||||
assert_equal ['alphabetic', 'as', 'expected'], Variable.new("words | sort").render(@context)
|
||||
assert_equal [3], Variable.new("value | sort").render(@context)
|
||||
assert_equal ['are', 'flattened'], Variable.new("arrays | sort").render(@context)
|
||||
assert_equal '1 2 3 4', Template.parse("{{numbers | sort | join}}").render(@context)
|
||||
assert_equal 'alphabetic as expected', Template.parse("{{words | sort | join}}").render(@context)
|
||||
assert_equal '3', Template.parse("{{value | sort}}").render(@context)
|
||||
assert_equal 'are flower', Template.parse("{{arrays | sort | join}}").render(@context)
|
||||
assert_equal 'Expected case sensitive', Template.parse("{{case_sensitive | sort | join}}").render(@context)
|
||||
end
|
||||
|
||||
def test_sort_natural
|
||||
@context['words'] = ['case', 'Assert', 'Insensitive']
|
||||
@context['hashes'] = [{ 'a' => 'A' }, { 'a' => 'b' }, { 'a' => 'C' }]
|
||||
@context['objects'] = [TestObject.new('A'), TestObject.new('b'), TestObject.new('C')]
|
||||
|
||||
# Test strings
|
||||
assert_equal 'Assert case Insensitive', Template.parse("{{words | sort_natural | join}}").render(@context)
|
||||
|
||||
# Test hashes
|
||||
assert_equal 'A b C', Template.parse("{{hashes | sort_natural: 'a' | map: 'a' | join}}").render(@context)
|
||||
|
||||
# Test objects
|
||||
assert_equal 'A b C', Template.parse("{{objects | sort_natural: 'a' | map: 'a' | join}}").render(@context)
|
||||
end
|
||||
|
||||
def test_compact
|
||||
@context['words'] = ['a', nil, 'b', nil, 'c']
|
||||
@context['hashes'] = [{ 'a' => 'A' }, { 'a' => nil }, { 'a' => 'C' }]
|
||||
@context['objects'] = [TestObject.new('A'), TestObject.new(nil), TestObject.new('C')]
|
||||
|
||||
# Test strings
|
||||
assert_equal 'a b c', Template.parse("{{words | compact | join}}").render(@context)
|
||||
|
||||
# Test hashes
|
||||
assert_equal 'A C', Template.parse("{{hashes | compact: 'a' | map: 'a' | join}}").render(@context)
|
||||
|
||||
# Test objects
|
||||
assert_equal 'A C', Template.parse("{{objects | compact: 'a' | map: 'a' | join}}").render(@context)
|
||||
end
|
||||
|
||||
def test_strip_html
|
||||
@context['var'] = "<b>bla blub</a>"
|
||||
|
||||
assert_equal "bla blub", Variable.new("var | strip_html").render(@context)
|
||||
assert_equal "bla blub", Template.parse("{{ var | strip_html }}").render(@context)
|
||||
end
|
||||
|
||||
def test_strip_html_ignore_comments_with_html
|
||||
@context['var'] = "<!-- split and some <ul> tag --><b>bla blub</a>"
|
||||
|
||||
assert_equal "bla blub", Variable.new("var | strip_html").render(@context)
|
||||
assert_equal "bla blub", Template.parse("{{ var | strip_html }}").render(@context)
|
||||
end
|
||||
|
||||
def test_capitalize
|
||||
@context['var'] = "blub"
|
||||
|
||||
assert_equal "Blub", Variable.new("var | capitalize").render(@context)
|
||||
assert_equal "Blub", Template.parse("{{ var | capitalize }}").render(@context)
|
||||
end
|
||||
|
||||
def test_nonexistent_filter_is_ignored
|
||||
@context['var'] = 1000
|
||||
|
||||
assert_equal 1000, Variable.new("var | xyzzy").render(@context)
|
||||
assert_equal '1000', Template.parse("{{ var | xyzzy }}").render(@context)
|
||||
end
|
||||
|
||||
def test_filter_with_keyword_arguments
|
||||
@context['surname'] = 'john'
|
||||
@context['input'] = 'hello %{first_name}, %{last_name}'
|
||||
@context.add_filters(SubstituteFilter)
|
||||
output = Variable.new(%! 'hello %{first_name}, %{last_name}' | substitute: first_name: surname, last_name: 'doe' !).render(@context)
|
||||
output = Template.parse(%({{ input | substitute: first_name: surname, last_name: 'doe' }})).render(@context)
|
||||
assert_equal 'hello john, doe', output
|
||||
end
|
||||
|
||||
def test_override_object_method_in_filter
|
||||
assert_equal "tap overridden", Template.parse("{{var | tap}}").render!({ 'var' => 1000 }, filters: [OverrideObjectMethodFilter])
|
||||
|
||||
# tap still treated as a non-existent filter
|
||||
assert_equal "1000", Template.parse("{{var | tap}}").render!({ 'var' => 1000 })
|
||||
end
|
||||
end
|
||||
|
||||
class FiltersInTemplate < Test::Unit::TestCase
|
||||
class FiltersInTemplate < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_local_global
|
||||
Template.register_filter(MoneyFilter)
|
||||
|
||||
assert_equal " 1000$ ", Template.parse("{{1000 | money}}").render!(nil, nil)
|
||||
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, :filters => CanadianMoneyFilter)
|
||||
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, :filters => [CanadianMoneyFilter])
|
||||
with_global_filter(MoneyFilter) do
|
||||
assert_equal " 1000$ ", Template.parse("{{1000 | money}}").render!(nil, nil)
|
||||
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, filters: CanadianMoneyFilter)
|
||||
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, filters: [CanadianMoneyFilter])
|
||||
end
|
||||
end
|
||||
|
||||
def test_local_filter_with_deprecated_syntax
|
||||
@@ -123,3 +169,10 @@ class FiltersInTemplate < Test::Unit::TestCase
|
||||
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, [CanadianMoneyFilter])
|
||||
end
|
||||
end # FiltersTest
|
||||
|
||||
class TestObject < Liquid::Drop
|
||||
attr_accessor :a
|
||||
def initialize(a)
|
||||
@a = a
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
require 'test_helper'
|
||||
|
||||
module MoneyFilter
|
||||
def money(input)
|
||||
sprintf(' %d$ ', input)
|
||||
class HashOrderingTest < Minitest::Test
|
||||
module MoneyFilter
|
||||
def money(input)
|
||||
sprintf(' %d$ ', input)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module CanadianMoneyFilter
|
||||
def money(input)
|
||||
sprintf(' %d$ CAD ', input)
|
||||
module CanadianMoneyFilter
|
||||
def money(input)
|
||||
sprintf(' %d$ CAD ', input)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class HashOrderingTest < Test::Unit::TestCase
|
||||
include Liquid
|
||||
|
||||
def test_global_register_order
|
||||
Template.register_filter(MoneyFilter)
|
||||
Template.register_filter(CanadianMoneyFilter)
|
||||
|
||||
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render(nil, nil)
|
||||
end
|
||||
|
||||
def test_global_register_order
|
||||
with_global_filter(MoneyFilter, CanadianMoneyFilter) do
|
||||
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render(nil, nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -14,7 +14,7 @@ module FunnyFilter
|
||||
end
|
||||
|
||||
def add_tag(input, tag = "p", id = "foo")
|
||||
%|<#{tag} id="#{id}">#{input}</#{tag}>|
|
||||
%(<#{tag} id="#{id}">#{input}</#{tag}>)
|
||||
end
|
||||
|
||||
def paragraph(input)
|
||||
@@ -22,95 +22,102 @@ module FunnyFilter
|
||||
end
|
||||
|
||||
def link_to(name, url)
|
||||
%|<a href="#{url}">#{name}</a>|
|
||||
%(<a href="#{url}">#{name}</a>)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
class OutputTest < Test::Unit::TestCase
|
||||
class OutputTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def setup
|
||||
@assigns = {
|
||||
'best_cars' => 'bmw',
|
||||
'car' => {'bmw' => 'good', 'gm' => 'bad'}
|
||||
}
|
||||
'car' => { 'bmw' => 'good', 'gm' => 'bad' }
|
||||
}
|
||||
end
|
||||
|
||||
def test_variable
|
||||
text = %| {{best_cars}} |
|
||||
text = %( {{best_cars}} )
|
||||
|
||||
expected = %| bmw |
|
||||
expected = %( bmw )
|
||||
assert_equal expected, Template.parse(text).render!(@assigns)
|
||||
end
|
||||
|
||||
def test_variable_traversing
|
||||
text = %| {{car.bmw}} {{car.gm}} {{car.bmw}} |
|
||||
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
|
||||
|
||||
expected = %| good bad good |
|
||||
def test_variable_traversing
|
||||
text = %( {{car.bmw}} {{car.gm}} {{car.bmw}} )
|
||||
|
||||
expected = %( good bad good )
|
||||
assert_equal expected, Template.parse(text).render!(@assigns)
|
||||
end
|
||||
|
||||
def test_variable_piping
|
||||
text = %( {{ car.gm | make_funny }} )
|
||||
expected = %| LOL |
|
||||
expected = %( LOL )
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter])
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
end
|
||||
|
||||
def test_variable_piping_with_input
|
||||
text = %( {{ car.gm | cite_funny }} )
|
||||
expected = %| LOL: bad |
|
||||
expected = %( LOL: bad )
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter])
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
end
|
||||
|
||||
def test_variable_piping_with_args
|
||||
text = %! {{ car.gm | add_smiley : ':-(' }} !
|
||||
expected = %| bad :-( |
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter])
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
end
|
||||
|
||||
def test_variable_piping_with_no_args
|
||||
text = %! {{ car.gm | add_smiley }} !
|
||||
text = %( {{ car.gm | add_smiley }} )
|
||||
expected = %| bad :-) |
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter])
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
end
|
||||
|
||||
def test_multiple_variable_piping_with_args
|
||||
text = %! {{ car.gm | add_smiley : ':-(' | add_smiley : ':-('}} !
|
||||
expected = %| bad :-( :-( |
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter])
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
end
|
||||
|
||||
def test_variable_piping_with_multiple_args
|
||||
text = %! {{ car.gm | add_tag : 'span', 'bar'}} !
|
||||
expected = %| <span id="bar">bad</span> |
|
||||
text = %( {{ car.gm | add_tag : 'span', 'bar'}} )
|
||||
expected = %( <span id="bar">bad</span> )
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter])
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
end
|
||||
|
||||
def test_variable_piping_with_variable_args
|
||||
text = %! {{ car.gm | add_tag : 'span', car.bmw}} !
|
||||
expected = %| <span id="good">bad</span> |
|
||||
text = %( {{ car.gm | add_tag : 'span', car.bmw}} )
|
||||
expected = %( <span id="good">bad</span> )
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter])
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
end
|
||||
|
||||
def test_multiple_pipings
|
||||
text = %( {{ best_cars | cite_funny | paragraph }} )
|
||||
expected = %| <p>LOL: bmw</p> |
|
||||
expected = %( <p>LOL: bmw</p> )
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter])
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
end
|
||||
|
||||
def test_link_to
|
||||
text = %( {{ 'Typo' | link_to: 'http://typo.leetsoft.com' }} )
|
||||
expected = %| <a href="http://typo.leetsoft.com">Typo</a> |
|
||||
expected = %( <a href="http://typo.leetsoft.com">Typo</a> )
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter])
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
end
|
||||
end # OutputTest
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
require 'test_helper'
|
||||
|
||||
class ParsingQuirksTest < Test::Unit::TestCase
|
||||
class ParsingQuirksTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_parsing_css
|
||||
@@ -9,38 +9,39 @@ class ParsingQuirksTest < Test::Unit::TestCase
|
||||
end
|
||||
|
||||
def test_raise_on_single_close_bracet
|
||||
assert_raise(SyntaxError) do
|
||||
assert_raises(SyntaxError) do
|
||||
Template.parse("text {{method} oh nos!")
|
||||
end
|
||||
end
|
||||
|
||||
def test_raise_on_label_and_no_close_bracets
|
||||
assert_raise(SyntaxError) do
|
||||
assert_raises(SyntaxError) do
|
||||
Template.parse("TEST {{ ")
|
||||
end
|
||||
end
|
||||
|
||||
def test_raise_on_label_and_no_close_bracets_percent
|
||||
assert_raise(SyntaxError) do
|
||||
assert_raises(SyntaxError) do
|
||||
Template.parse("TEST {% ")
|
||||
end
|
||||
end
|
||||
|
||||
def test_error_on_empty_filter
|
||||
assert_nothing_raised do
|
||||
Template.parse("{{test}}")
|
||||
Template.parse("{{|test}}")
|
||||
assert Template.parse("{{test}}")
|
||||
|
||||
with_error_mode(:lax) do
|
||||
assert Template.parse("{{|test}}")
|
||||
end
|
||||
|
||||
with_error_mode(:strict) do
|
||||
assert_raise(SyntaxError) do
|
||||
Template.parse("{{test |a|b|}}")
|
||||
end
|
||||
assert_raises(SyntaxError) { Template.parse("{{|test}}") }
|
||||
assert_raises(SyntaxError) { Template.parse("{{test |a|b|}}") }
|
||||
end
|
||||
end
|
||||
|
||||
def test_meaningless_parens_error
|
||||
with_error_mode(:strict) do
|
||||
assert_raise(SyntaxError) do
|
||||
assert_raises(SyntaxError) do
|
||||
markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false"
|
||||
Template.parse("{% if #{markup} %} YES {% endif %}")
|
||||
end
|
||||
@@ -49,11 +50,11 @@ class ParsingQuirksTest < Test::Unit::TestCase
|
||||
|
||||
def test_unexpected_characters_syntax_error
|
||||
with_error_mode(:strict) do
|
||||
assert_raise(SyntaxError) do
|
||||
assert_raises(SyntaxError) do
|
||||
markup = "true && false"
|
||||
Template.parse("{% if #{markup} %} YES {% endif %}")
|
||||
end
|
||||
assert_raise(SyntaxError) do
|
||||
assert_raises(SyntaxError) do
|
||||
markup = "false || true"
|
||||
Template.parse("{% if #{markup} %} YES {% endif %}")
|
||||
end
|
||||
@@ -61,27 +62,57 @@ class ParsingQuirksTest < Test::Unit::TestCase
|
||||
end
|
||||
|
||||
def test_no_error_on_lax_empty_filter
|
||||
assert_nothing_raised do
|
||||
Template.parse("{{test |a|b|}}", :error_mode => :lax)
|
||||
Template.parse("{{test}}", :error_mode => :lax)
|
||||
Template.parse("{{|test|}}", :error_mode => :lax)
|
||||
end
|
||||
assert Template.parse("{{test |a|b|}}", error_mode: :lax)
|
||||
assert Template.parse("{{test}}", error_mode: :lax)
|
||||
assert Template.parse("{{|test|}}", error_mode: :lax)
|
||||
end
|
||||
|
||||
def test_meaningless_parens_lax
|
||||
with_error_mode(:lax) do
|
||||
assigns = {'b' => 'bar', 'c' => 'baz'}
|
||||
assigns = { 'b' => 'bar', 'c' => 'baz' }
|
||||
markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false"
|
||||
assert_template_result(' YES ',"{% if #{markup} %} YES {% endif %}", assigns)
|
||||
assert_template_result(' YES ', "{% if #{markup} %} YES {% endif %}", assigns)
|
||||
end
|
||||
end
|
||||
|
||||
def test_unexpected_characters_silently_eat_logic_lax
|
||||
with_error_mode(:lax) do
|
||||
markup = "true && false"
|
||||
assert_template_result(' YES ',"{% if #{markup} %} YES {% endif %}")
|
||||
assert_template_result(' YES ', "{% if #{markup} %} YES {% endif %}")
|
||||
markup = "false || true"
|
||||
assert_template_result('',"{% if #{markup} %} YES {% endif %}")
|
||||
assert_template_result('', "{% if #{markup} %} YES {% endif %}")
|
||||
end
|
||||
end
|
||||
|
||||
def test_raise_on_invalid_tag_delimiter
|
||||
assert_raises(Liquid::SyntaxError) do
|
||||
Template.new.parse('{% end %}')
|
||||
end
|
||||
end
|
||||
|
||||
def test_unanchored_filter_arguments
|
||||
with_error_mode(:lax) do
|
||||
assert_template_result('hi', "{{ 'hi there' | split$$$:' ' | first }}")
|
||||
|
||||
assert_template_result('x', "{{ 'X' | downcase) }}")
|
||||
|
||||
# After the messed up quotes a filter without parameters (reverse) should work
|
||||
# but one with parameters (remove) shouldn't be detected.
|
||||
assert_template_result('here', "{{ 'hi there' | split:\"t\"\" | reverse | first}}")
|
||||
assert_template_result('hi ', "{{ 'hi there' | split:\"t\"\" | remove:\"i\" | first}}")
|
||||
end
|
||||
end
|
||||
|
||||
def test_invalid_variables_work
|
||||
with_error_mode(:lax) do
|
||||
assert_template_result('bar', "{% assign 123foo = 'bar' %}{{ 123foo }}")
|
||||
assert_template_result('123', "{% assign 123 = 'bar' %}{{ 123 }}")
|
||||
end
|
||||
end
|
||||
|
||||
def test_extra_dots_in_ranges
|
||||
with_error_mode(:lax) do
|
||||
assert_template_result('12345', "{% for i in (1...5) %}{{ i }}{% endfor %}")
|
||||
end
|
||||
end
|
||||
end # ParsingQuirksTest
|
||||
|
||||
154
test/integration/render_profiling_test.rb
Normal file
154
test/integration/render_profiling_test.rb
Normal file
@@ -0,0 +1,154 @@
|
||||
require 'test_helper'
|
||||
|
||||
class RenderProfilingTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
class ProfilingFileSystem
|
||||
def read_template_file(template_path)
|
||||
"Rendering template {% assign template_name = '#{template_path}'%}\n{{ template_name }}"
|
||||
end
|
||||
end
|
||||
|
||||
def setup
|
||||
Liquid::Template.file_system = ProfilingFileSystem.new
|
||||
end
|
||||
|
||||
def test_template_allows_flagging_profiling
|
||||
t = Template.parse("{{ 'a string' | upcase }}")
|
||||
t.render!
|
||||
|
||||
assert_nil t.profiler
|
||||
end
|
||||
|
||||
def test_parse_makes_available_simple_profiling
|
||||
t = Template.parse("{{ 'a string' | upcase }}", profile: true)
|
||||
t.render!
|
||||
|
||||
assert_equal 1, t.profiler.length
|
||||
|
||||
node = t.profiler[0]
|
||||
assert_equal " 'a string' | upcase ", node.code
|
||||
end
|
||||
|
||||
def test_render_ignores_raw_strings_when_profiling
|
||||
t = Template.parse("This is raw string\nstuff\nNewline", profile: true)
|
||||
t.render!
|
||||
|
||||
assert_equal 0, t.profiler.length
|
||||
end
|
||||
|
||||
def test_profiling_includes_line_numbers_of_liquid_nodes
|
||||
t = Template.parse("{{ 'a string' | upcase }}\n{% increment test %}", profile: true)
|
||||
t.render!
|
||||
assert_equal 2, t.profiler.length
|
||||
|
||||
# {{ 'a string' | upcase }}
|
||||
assert_equal 1, t.profiler[0].line_number
|
||||
# {{ increment test }}
|
||||
assert_equal 2, t.profiler[1].line_number
|
||||
end
|
||||
|
||||
def test_profiling_includes_line_numbers_of_included_partials
|
||||
t = Template.parse("{% include 'a_template' %}", profile: true)
|
||||
t.render!
|
||||
|
||||
included_children = t.profiler[0].children
|
||||
|
||||
# {% assign template_name = 'a_template' %}
|
||||
assert_equal 1, included_children[0].line_number
|
||||
# {{ template_name }}
|
||||
assert_equal 2, included_children[1].line_number
|
||||
end
|
||||
|
||||
def test_profiling_times_the_rendering_of_tokens
|
||||
t = Template.parse("{% include 'a_template' %}", profile: true)
|
||||
t.render!
|
||||
|
||||
node = t.profiler[0]
|
||||
refute_nil node.render_time
|
||||
end
|
||||
|
||||
def test_profiling_times_the_entire_render
|
||||
t = Template.parse("{% include 'a_template' %}", profile: true)
|
||||
t.render!
|
||||
|
||||
assert t.profiler.total_render_time >= 0, "Total render time was not calculated"
|
||||
end
|
||||
|
||||
def test_profiling_uses_include_to_mark_children
|
||||
t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}", profile: true)
|
||||
t.render!
|
||||
|
||||
include_node = t.profiler[1]
|
||||
assert_equal 2, include_node.children.length
|
||||
end
|
||||
|
||||
def test_profiling_marks_children_with_the_name_of_included_partial
|
||||
t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}", profile: true)
|
||||
t.render!
|
||||
|
||||
include_node = t.profiler[1]
|
||||
include_node.children.each do |child|
|
||||
assert_equal "a_template", child.partial
|
||||
end
|
||||
end
|
||||
|
||||
def test_profiling_supports_multiple_templates
|
||||
t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}\n{% include 'b_template' %}", profile: true)
|
||||
t.render!
|
||||
|
||||
a_template = t.profiler[1]
|
||||
a_template.children.each do |child|
|
||||
assert_equal "a_template", child.partial
|
||||
end
|
||||
|
||||
b_template = t.profiler[2]
|
||||
b_template.children.each do |child|
|
||||
assert_equal "b_template", child.partial
|
||||
end
|
||||
end
|
||||
|
||||
def test_profiling_supports_rendering_the_same_partial_multiple_times
|
||||
t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}\n{% include 'a_template' %}", profile: true)
|
||||
t.render!
|
||||
|
||||
a_template1 = t.profiler[1]
|
||||
a_template1.children.each do |child|
|
||||
assert_equal "a_template", child.partial
|
||||
end
|
||||
|
||||
a_template2 = t.profiler[2]
|
||||
a_template2.children.each do |child|
|
||||
assert_equal "a_template", child.partial
|
||||
end
|
||||
end
|
||||
|
||||
def test_can_iterate_over_each_profiling_entry
|
||||
t = Template.parse("{{ 'a string' | upcase }}\n{% increment test %}", profile: true)
|
||||
t.render!
|
||||
|
||||
timing_count = 0
|
||||
t.profiler.each do |timing|
|
||||
timing_count += 1
|
||||
end
|
||||
|
||||
assert_equal 2, timing_count
|
||||
end
|
||||
|
||||
def test_profiling_marks_children_of_if_blocks
|
||||
t = Template.parse("{% if true %} {% increment test %} {{ test }} {% endif %}", profile: true)
|
||||
t.render!
|
||||
|
||||
assert_equal 1, t.profiler.length
|
||||
assert_equal 2, t.profiler[0].children.length
|
||||
end
|
||||
|
||||
def test_profiling_marks_children_of_for_blocks
|
||||
t = Template.parse("{% for item in collection %} {{ item }} {% endfor %}", profile: true)
|
||||
t.render!({ "collection" => ["one", "two"] })
|
||||
|
||||
assert_equal 1, t.profiler.length
|
||||
# Will profile each invocation of the for block
|
||||
assert_equal 2, t.profiler[0].children.length
|
||||
end
|
||||
end
|
||||
@@ -6,37 +6,39 @@ module SecurityFilter
|
||||
end
|
||||
end
|
||||
|
||||
class SecurityTest < Test::Unit::TestCase
|
||||
class SecurityTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def setup
|
||||
@assigns = {}
|
||||
end
|
||||
|
||||
def test_no_instance_eval
|
||||
text = %( {{ '1+1' | instance_eval }} )
|
||||
expected = %| 1+1 |
|
||||
expected = %( 1+1 )
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns)
|
||||
end
|
||||
|
||||
def test_no_existing_instance_eval
|
||||
text = %( {{ '1+1' | __instance_eval__ }} )
|
||||
expected = %| 1+1 |
|
||||
expected = %( 1+1 )
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns)
|
||||
end
|
||||
|
||||
|
||||
def test_no_instance_eval_after_mixing_in_new_filter
|
||||
text = %( {{ '1+1' | instance_eval }} )
|
||||
expected = %| 1+1 |
|
||||
expected = %( 1+1 )
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns)
|
||||
end
|
||||
|
||||
|
||||
def test_no_instance_eval_later_in_chain
|
||||
text = %( {{ '1+1' | add_one | instance_eval }} )
|
||||
expected = %| 1+1 + 1 |
|
||||
expected = %( 1+1 + 1 )
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, :filters => SecurityFilter)
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: SecurityFilter)
|
||||
end
|
||||
|
||||
def test_does_not_add_filters_to_symbol_table
|
||||
|
||||
@@ -7,6 +7,8 @@ class Filters
|
||||
end
|
||||
|
||||
class TestThing
|
||||
attr_reader :foo
|
||||
|
||||
def initialize
|
||||
@foo = 0
|
||||
end
|
||||
@@ -39,7 +41,17 @@ class TestEnumerable < Liquid::Drop
|
||||
end
|
||||
end
|
||||
|
||||
class StandardFiltersTest < Test::Unit::TestCase
|
||||
class NumberLikeThing < Liquid::Drop
|
||||
def initialize(amount)
|
||||
@amount = amount
|
||||
end
|
||||
|
||||
def to_number
|
||||
@amount
|
||||
end
|
||||
end
|
||||
|
||||
class StandardFiltersTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def setup
|
||||
@@ -47,7 +59,7 @@ class StandardFiltersTest < Test::Unit::TestCase
|
||||
end
|
||||
|
||||
def test_size
|
||||
assert_equal 3, @filters.size([1,2,3])
|
||||
assert_equal 3, @filters.size([1, 2, 3])
|
||||
assert_equal 0, @filters.size([])
|
||||
assert_equal 0, @filters.size(nil)
|
||||
end
|
||||
@@ -62,6 +74,41 @@ class StandardFiltersTest < Test::Unit::TestCase
|
||||
assert_equal '', @filters.upcase(nil)
|
||||
end
|
||||
|
||||
def test_slice
|
||||
assert_equal 'oob', @filters.slice('foobar', 1, 3)
|
||||
assert_equal 'oobar', @filters.slice('foobar', 1, 1000)
|
||||
assert_equal '', @filters.slice('foobar', 1, 0)
|
||||
assert_equal 'o', @filters.slice('foobar', 1, 1)
|
||||
assert_equal 'bar', @filters.slice('foobar', 3, 3)
|
||||
assert_equal 'ar', @filters.slice('foobar', -2, 2)
|
||||
assert_equal 'ar', @filters.slice('foobar', -2, 1000)
|
||||
assert_equal 'r', @filters.slice('foobar', -1)
|
||||
assert_equal '', @filters.slice(nil, 0)
|
||||
assert_equal '', @filters.slice('foobar', 100, 10)
|
||||
assert_equal '', @filters.slice('foobar', -100, 10)
|
||||
assert_equal 'oob', @filters.slice('foobar', '1', '3')
|
||||
assert_raises(Liquid::ArgumentError) do
|
||||
@filters.slice('foobar', nil)
|
||||
end
|
||||
assert_raises(Liquid::ArgumentError) do
|
||||
@filters.slice('foobar', 0, "")
|
||||
end
|
||||
end
|
||||
|
||||
def test_slice_on_arrays
|
||||
input = 'foobar'.split(//)
|
||||
assert_equal %w(o o b), @filters.slice(input, 1, 3)
|
||||
assert_equal %w(o o b a r), @filters.slice(input, 1, 1000)
|
||||
assert_equal %w(), @filters.slice(input, 1, 0)
|
||||
assert_equal %w(o), @filters.slice(input, 1, 1)
|
||||
assert_equal %w(b a r), @filters.slice(input, 3, 3)
|
||||
assert_equal %w(a r), @filters.slice(input, -2, 2)
|
||||
assert_equal %w(a r), @filters.slice(input, -2, 1000)
|
||||
assert_equal %w(r), @filters.slice(input, -1)
|
||||
assert_equal %w(), @filters.slice(input, 100, 10)
|
||||
assert_equal %w(), @filters.slice(input, -100, 10)
|
||||
end
|
||||
|
||||
def test_truncate
|
||||
assert_equal '1234...', @filters.truncate('1234567890', 7)
|
||||
assert_equal '1234567890', @filters.truncate('1234567890', 20)
|
||||
@@ -71,15 +118,17 @@ class StandardFiltersTest < Test::Unit::TestCase
|
||||
end
|
||||
|
||||
def test_split
|
||||
assert_equal ['12','34'], @filters.split('12~34', '~')
|
||||
assert_equal ['A? ',' ,Z'], @filters.split('A? ~ ~ ~ ,Z', '~ ~ ~')
|
||||
assert_equal ['12', '34'], @filters.split('12~34', '~')
|
||||
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 ['A', 'Z'], @filters.split('AxZ', /x/)
|
||||
assert_equal [], @filters.split(nil, ' ')
|
||||
end
|
||||
|
||||
def test_escape
|
||||
assert_equal '<strong>', @filters.escape('<strong>')
|
||||
assert_equal nil, @filters.escape(nil)
|
||||
assert_equal '<strong>', @filters.h('<strong>')
|
||||
end
|
||||
|
||||
@@ -87,6 +136,18 @@ class StandardFiltersTest < Test::Unit::TestCase
|
||||
assert_equal '<strong>Hulk</strong>', @filters.escape_once('<strong>Hulk</strong>')
|
||||
end
|
||||
|
||||
def test_url_encode
|
||||
assert_equal 'foo%2B1%40example.com', @filters.url_encode('foo+1@example.com')
|
||||
assert_equal 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 nil, @filters.url_decode(nil)
|
||||
end
|
||||
|
||||
def test_truncatewords
|
||||
assert_equal 'one two three', @filters.truncatewords('one two three', 4)
|
||||
assert_equal 'one two...', @filters.truncatewords('one two three', 2)
|
||||
@@ -106,30 +167,62 @@ class StandardFiltersTest < Test::Unit::TestCase
|
||||
end
|
||||
|
||||
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], ' - ')
|
||||
end
|
||||
|
||||
def test_sort
|
||||
assert_equal [1,2,3,4], @filters.sort([4,3,2,1])
|
||||
assert_equal [{"a" => 1}, {"a" => 2}, {"a" => 3}, {"a" => 4}], @filters.sort([{"a" => 4}, {"a" => 3}, {"a" => 1}, {"a" => 2}], "a")
|
||||
assert_equal [1, 2, 3, 4], @filters.sort([4, 3, 2, 1])
|
||||
assert_equal [{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }], @filters.sort([{ "a" => 4 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a")
|
||||
end
|
||||
|
||||
def test_sort_empty_array
|
||||
assert_equal [], @filters.sort([], "a")
|
||||
end
|
||||
|
||||
def test_sort_natural_empty_array
|
||||
assert_equal [], @filters.sort_natural([], "a")
|
||||
end
|
||||
|
||||
def test_legacy_sort_hash
|
||||
assert_equal [{ a: 1, b: 2 }], @filters.sort({ a: 1, b: 2 })
|
||||
end
|
||||
|
||||
def test_numerical_vs_lexicographical_sort
|
||||
assert_equal [2, 10], @filters.sort([10, 2])
|
||||
assert_equal [{"a" => 2}, {"a" => 10}], @filters.sort([{"a" => 10}, {"a" => 2}], "a")
|
||||
assert_equal [{ "a" => 2 }, { "a" => 10 }], @filters.sort([{ "a" => 10 }, { "a" => 2 }], "a")
|
||||
assert_equal ["10", "2"], @filters.sort(["10", "2"])
|
||||
assert_equal [{"a" => "10"}, {"a" => "2"}], @filters.sort([{"a" => "10"}, {"a" => "2"}], "a")
|
||||
assert_equal [{ "a" => "10" }, { "a" => "2" }], @filters.sort([{ "a" => "10" }, { "a" => "2" }], "a")
|
||||
end
|
||||
|
||||
def test_uniq
|
||||
assert_equal ["foo"], @filters.uniq("foo")
|
||||
assert_equal [1, 3, 2, 4], @filters.uniq([1, 1, 3, 2, 3, 1, 4, 3, 2, 1])
|
||||
assert_equal [{ "a" => 1 }, { "a" => 3 }, { "a" => 2 }], @filters.uniq([{ "a" => 1 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a")
|
||||
testdrop = TestDrop.new
|
||||
assert_equal [testdrop], @filters.uniq([testdrop, TestDrop.new], 'test')
|
||||
end
|
||||
|
||||
def test_uniq_empty_array
|
||||
assert_equal [], @filters.uniq([], "a")
|
||||
end
|
||||
|
||||
def test_compact_empty_array
|
||||
assert_equal [], @filters.compact([], "a")
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def test_legacy_reverse_hash
|
||||
assert_equal [{ a: 1, b: 2 }], @filters.reverse(a: 1, b: 2)
|
||||
end
|
||||
|
||||
def test_map
|
||||
assert_equal [1,2,3,4], @filters.map([{"a" => 1}, {"a" => 2}, {"a" => 3}, {"a" => 4}], 'a')
|
||||
assert_equal [1, 2, 3, 4], @filters.map([{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }], 'a')
|
||||
assert_template_result 'abc', "{{ ary | map:'foo' | map:'bar' }}",
|
||||
'ary' => [{'foo' => {'bar' => 'a'}}, {'foo' => {'bar' => 'b'}}, {'foo' => {'bar' => 'c'}}]
|
||||
'ary' => [{ 'foo' => { 'bar' => 'a' } }, { 'foo' => { 'bar' => 'b' } }, { 'foo' => { 'bar' => 'c' } }]
|
||||
end
|
||||
|
||||
def test_map_doesnt_call_arbitrary_stuff
|
||||
@@ -147,18 +240,38 @@ class StandardFiltersTest < Test::Unit::TestCase
|
||||
"thing" => { "foo" => [ { "bar" => 42 }, { "bar" => 17 } ] }
|
||||
end
|
||||
|
||||
def test_legacy_map_on_hashes_with_dynamic_key
|
||||
template = "{% assign key = 'foo' %}{{ thing | map: key | map: 'bar' }}"
|
||||
hash = { "foo" => { "bar" => 42 } }
|
||||
assert_template_result "42", template, "thing" => hash
|
||||
end
|
||||
|
||||
def test_sort_calls_to_liquid
|
||||
t = TestThing.new
|
||||
assert_template_result "woot: 1", '{{ foo | sort: "whatever" }}', "foo" => [t]
|
||||
Liquid::Template.parse('{{ foo | sort: "whatever" }}').render("foo" => [t])
|
||||
assert t.foo > 0
|
||||
end
|
||||
|
||||
def test_map_over_proc
|
||||
drop = TestDrop.new
|
||||
p = Proc.new{ drop }
|
||||
p = proc{ drop }
|
||||
templ = '{{ procs | map: "test" }}'
|
||||
assert_template_result "testfoo", templ, "procs" => [p]
|
||||
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
|
||||
assert_template_result "123", '{{ foo | map: "foo" }}', "foo" => TestEnumerable.new
|
||||
end
|
||||
@@ -167,6 +280,15 @@ class StandardFiltersTest < Test::Unit::TestCase
|
||||
assert_template_result "213", '{{ foo | sort: "bar" | map: "foo" }}', "foo" => TestEnumerable.new
|
||||
end
|
||||
|
||||
def test_first_and_last_call_to_liquid
|
||||
assert_template_result 'foobar', '{{ foo | first }}', 'foo' => [ThingWithToLiquid.new]
|
||||
assert_template_result 'foobar', '{{ foo | last }}', 'foo' => [ThingWithToLiquid.new]
|
||||
end
|
||||
|
||||
def test_truncate_calls_to_liquid
|
||||
assert_template_result "wo...", '{{ foo | truncate: 5 }}', "foo" => TestThing.new
|
||||
end
|
||||
|
||||
def test_date
|
||||
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")
|
||||
@@ -186,29 +308,38 @@ class StandardFiltersTest < Test::Unit::TestCase
|
||||
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}", @filters.date('today', '%Y')
|
||||
assert_equal "#{Date.today.year}", @filters.date('Today', '%Y')
|
||||
|
||||
assert_equal nil, @filters.date(nil, "%B")
|
||||
|
||||
assert_equal "07/05/2006", @filters.date(1152098955, "%m/%d/%Y")
|
||||
assert_equal "07/05/2006", @filters.date("1152098955", "%m/%d/%Y")
|
||||
assert_equal '', @filters.date('', "%B")
|
||||
|
||||
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")
|
||||
end
|
||||
end
|
||||
|
||||
def test_first_last
|
||||
assert_equal 1, @filters.first([1,2,3])
|
||||
assert_equal 3, @filters.last([1,2,3])
|
||||
assert_equal 1, @filters.first([1, 2, 3])
|
||||
assert_equal 3, @filters.last([1, 2, 3])
|
||||
assert_equal nil, @filters.first([])
|
||||
assert_equal nil, @filters.last([])
|
||||
end
|
||||
|
||||
def test_replace
|
||||
assert_equal '2 2 2 2', @filters.replace('1 1 1 1', '1', 2)
|
||||
assert_equal '2 2 2 2', @filters.replace('1 1 1 1', 1, 2)
|
||||
assert_equal '2 1 1 1', @filters.replace_first('1 1 1 1', '1', 2)
|
||||
assert_equal '2 1 1 1', @filters.replace_first('1 1 1 1', 1, 2)
|
||||
assert_template_result '2 1 1 1', "{{ '1 1 1 1' | replace_first: '1', 2 }}"
|
||||
end
|
||||
|
||||
def test_remove
|
||||
assert_equal ' ', @filters.remove("a a a a", 'a')
|
||||
assert_equal ' ', @filters.remove("1 1 1 1", 1)
|
||||
assert_equal 'a a a', @filters.remove_first("a a a a", 'a ')
|
||||
assert_equal ' 1 1 1', @filters.remove_first("1 1 1 1", 1)
|
||||
assert_template_result 'a a a', "{{ 'a a a a' | remove_first: 'a ' }}"
|
||||
end
|
||||
|
||||
@@ -243,20 +374,25 @@ class StandardFiltersTest < Test::Unit::TestCase
|
||||
def test_plus
|
||||
assert_template_result "2", "{{ 1 | plus:1 }}"
|
||||
assert_template_result "2.0", "{{ '1' | plus:'1.0' }}"
|
||||
|
||||
assert_template_result "5", "{{ price | plus:'2' }}", 'price' => NumberLikeThing.new(3)
|
||||
end
|
||||
|
||||
def test_minus
|
||||
assert_template_result "4", "{{ input | minus:operand }}", 'input' => 5, 'operand' => 1
|
||||
assert_template_result "2.3", "{{ '4.3' | minus:'2' }}"
|
||||
|
||||
assert_template_result "5", "{{ price | minus:'2' }}", 'price' => NumberLikeThing.new(7)
|
||||
end
|
||||
|
||||
def test_times
|
||||
assert_template_result "12", "{{ 3 | times:4 }}"
|
||||
assert_template_result "0", "{{ 'foo' | times:4 }}"
|
||||
|
||||
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 "4", "{{ price | times:2 }}", 'price' => NumberLikeThing.new(2)
|
||||
end
|
||||
|
||||
def test_divided_by
|
||||
@@ -267,38 +403,74 @@ class StandardFiltersTest < Test::Unit::TestCase
|
||||
assert_equal "Liquid error: divided by 0", Template.parse("{{ 5 | divided_by:0 }}").render
|
||||
|
||||
assert_template_result "0.5", "{{ 2.0 | divided_by:4 }}"
|
||||
assert_raises(Liquid::ZeroDivisionError) do
|
||||
assert_template_result "4", "{{ 1 | modulo: 0 }}"
|
||||
end
|
||||
|
||||
assert_template_result "5", "{{ price | divided_by:2 }}", 'price' => NumberLikeThing.new(10)
|
||||
end
|
||||
|
||||
def test_modulo
|
||||
assert_template_result "1", "{{ 3 | modulo:2 }}"
|
||||
assert_raises(Liquid::ZeroDivisionError) do
|
||||
assert_template_result "4", "{{ 1 | modulo: 0 }}"
|
||||
end
|
||||
|
||||
assert_template_result "1", "{{ price | modulo:2 }}", 'price' => NumberLikeThing.new(3)
|
||||
end
|
||||
|
||||
def test_round
|
||||
assert_template_result "5", "{{ input | round }}", 'input' => 4.6
|
||||
assert_template_result "4", "{{ '4.3' | round }}"
|
||||
assert_template_result "4.56", "{{ input | round: 2 }}", 'input' => 4.5612
|
||||
assert_raises(Liquid::FloatDomainError) do
|
||||
assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | round }}"
|
||||
end
|
||||
|
||||
assert_template_result "5", "{{ price | round }}", 'price' => NumberLikeThing.new(4.6)
|
||||
assert_template_result "4", "{{ price | round }}", 'price' => NumberLikeThing.new(4.3)
|
||||
end
|
||||
|
||||
def test_ceil
|
||||
assert_template_result "5", "{{ input | ceil }}", 'input' => 4.6
|
||||
assert_template_result "5", "{{ '4.3' | ceil }}"
|
||||
assert_raises(Liquid::FloatDomainError) do
|
||||
assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | ceil }}"
|
||||
end
|
||||
|
||||
assert_template_result "5", "{{ price | ceil }}", 'price' => NumberLikeThing.new(4.6)
|
||||
end
|
||||
|
||||
def test_floor
|
||||
assert_template_result "4", "{{ input | floor }}", 'input' => 4.6
|
||||
assert_template_result "4", "{{ '4.3' | floor }}"
|
||||
assert_raises(Liquid::FloatDomainError) do
|
||||
assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | floor }}"
|
||||
end
|
||||
|
||||
assert_template_result "5", "{{ price | floor }}", 'price' => NumberLikeThing.new(5.4)
|
||||
end
|
||||
|
||||
def test_append
|
||||
assigns = {'a' => 'bc', 'b' => 'd' }
|
||||
assert_template_result('bcd',"{{ a | append: 'd'}}",assigns)
|
||||
assert_template_result('bcd',"{{ a | append: b}}",assigns)
|
||||
assigns = { 'a' => 'bc', 'b' => 'd' }
|
||||
assert_template_result('bcd', "{{ a | append: 'd'}}", assigns)
|
||||
assert_template_result('bcd', "{{ a | append: b}}", assigns)
|
||||
end
|
||||
|
||||
def test_concat
|
||||
assert_equal [1, 2, 3, 4], @filters.concat([1, 2], [3, 4])
|
||||
assert_equal [1, 2, 'a'], @filters.concat([1, 2], ['a'])
|
||||
assert_equal [1, 2, 10], @filters.concat([1, 2], [10])
|
||||
|
||||
assert_raises(Liquid::ArgumentError, "concat filter requires an array argument") do
|
||||
@filters.concat([1, 2], 10)
|
||||
end
|
||||
end
|
||||
|
||||
def test_prepend
|
||||
assigns = {'a' => 'bc', 'b' => 'a' }
|
||||
assert_template_result('abc',"{{ a | prepend: 'a'}}",assigns)
|
||||
assert_template_result('abc',"{{ a | prepend: b}}",assigns)
|
||||
assigns = { 'a' => 'bc', 'b' => 'a' }
|
||||
assert_template_result('abc', "{{ a | prepend: 'a'}}", assigns)
|
||||
assert_template_result('abc', "{{ a | prepend: b}}", assigns)
|
||||
end
|
||||
|
||||
def test_default
|
||||
@@ -311,6 +483,21 @@ class StandardFiltersTest < Test::Unit::TestCase
|
||||
end
|
||||
|
||||
def test_cannot_access_private_methods
|
||||
assert_template_result('a',"{{ 'a' | to_number }}")
|
||||
assert_template_result('a', "{{ 'a' | to_number }}")
|
||||
end
|
||||
|
||||
def test_date_raises_nothing
|
||||
assert_template_result('', "{{ '' | date: '%D' }}")
|
||||
assert_template_result('abc', "{{ 'abc' | date: '%D' }}")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def with_timezone(tz)
|
||||
old_tz = ENV['TZ']
|
||||
ENV['TZ'] = tz
|
||||
yield
|
||||
ensure
|
||||
ENV['TZ'] = old_tz
|
||||
end
|
||||
end # StandardFiltersTest
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
require 'test_helper'
|
||||
|
||||
class BreakTagTest < Test::Unit::TestCase
|
||||
class BreakTagTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
# tests that no weird errors are raised if break is called outside of a
|
||||
# block
|
||||
def test_break_with_no_block
|
||||
assigns = {'i' => 1}
|
||||
assigns = { 'i' => 1 }
|
||||
markup = '{% break %}'
|
||||
expected = ''
|
||||
|
||||
assert_template_result(expected, markup, assigns)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
require 'test_helper'
|
||||
|
||||
class ContinueTagTest < Test::Unit::TestCase
|
||||
class ContinueTagTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
# tests that no weird errors are raised if continue is called outside of a
|
||||
@@ -12,5 +12,4 @@ class ContinueTagTest < Test::Unit::TestCase
|
||||
|
||||
assert_template_result(expected, markup, assigns)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -6,14 +6,14 @@ class ThingWithValue < Liquid::Drop
|
||||
end
|
||||
end
|
||||
|
||||
class ForTagTest < Test::Unit::TestCase
|
||||
class ForTagTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_for
|
||||
assert_template_result(' yo yo yo yo ','{%for item in array%} yo {%endfor%}','array' => [1,2,3,4])
|
||||
assert_template_result('yoyo','{%for item in array%}yo{%endfor%}','array' => [1,2])
|
||||
assert_template_result(' yo ','{%for item in array%} yo {%endfor%}','array' => [1])
|
||||
assert_template_result('','{%for item in array%}{%endfor%}','array' => [1,2])
|
||||
assert_template_result(' yo yo yo yo ', '{%for item in array%} yo {%endfor%}', 'array' => [1, 2, 3, 4])
|
||||
assert_template_result('yoyo', '{%for item in array%}yo{%endfor%}', 'array' => [1, 2])
|
||||
assert_template_result(' yo ', '{%for item in array%} yo {%endfor%}', 'array' => [1])
|
||||
assert_template_result('', '{%for item in array%}{%endfor%}', 'array' => [1, 2])
|
||||
expected = <<HERE
|
||||
|
||||
yo
|
||||
@@ -28,46 +28,52 @@ HERE
|
||||
yo
|
||||
{%endfor%}
|
||||
HERE
|
||||
assert_template_result(expected,template,'array' => [1,2,3])
|
||||
assert_template_result(expected, template, 'array' => [1, 2, 3])
|
||||
end
|
||||
|
||||
def test_for_reversed
|
||||
assigns = {'array' => [ 1, 2, 3] }
|
||||
assert_template_result('321','{%for item in array reversed %}{{item}}{%endfor%}',assigns)
|
||||
assigns = { 'array' => [ 1, 2, 3] }
|
||||
assert_template_result('321', '{%for item in array reversed %}{{item}}{%endfor%}', assigns)
|
||||
end
|
||||
|
||||
def test_for_with_range
|
||||
assert_template_result(' 1 2 3 ','{%for item in (1..3) %} {{item}} {%endfor%}')
|
||||
assert_template_result(' 1 2 3 ', '{%for item in (1..3) %} {{item}} {%endfor%}')
|
||||
|
||||
assert_raises(Liquid::ArgumentError) do
|
||||
Template.parse('{% for i in (a..2) %}{% endfor %}').render!("a" => [1, 2])
|
||||
end
|
||||
|
||||
assert_template_result(' 0 1 2 3 ', '{% for item in (a..3) %} {{item}} {% endfor %}', "a" => "invalid integer")
|
||||
end
|
||||
|
||||
def test_for_with_variable_range
|
||||
assert_template_result(' 1 2 3 ','{%for item in (1..foobar) %} {{item}} {%endfor%}', "foobar" => 3)
|
||||
assert_template_result(' 1 2 3 ', '{%for item in (1..foobar) %} {{item}} {%endfor%}', "foobar" => 3)
|
||||
end
|
||||
|
||||
def test_for_with_hash_value_range
|
||||
foobar = { "value" => 3 }
|
||||
assert_template_result(' 1 2 3 ','{%for item in (1..foobar.value) %} {{item}} {%endfor%}', "foobar" => foobar)
|
||||
assert_template_result(' 1 2 3 ', '{%for item in (1..foobar.value) %} {{item}} {%endfor%}', "foobar" => foobar)
|
||||
end
|
||||
|
||||
def test_for_with_drop_value_range
|
||||
foobar = ThingWithValue.new
|
||||
assert_template_result(' 1 2 3 ','{%for item in (1..foobar.value) %} {{item}} {%endfor%}', "foobar" => foobar)
|
||||
assert_template_result(' 1 2 3 ', '{%for item in (1..foobar.value) %} {{item}} {%endfor%}', "foobar" => foobar)
|
||||
end
|
||||
|
||||
def test_for_with_variable
|
||||
assert_template_result(' 1 2 3 ','{%for item in array%} {{item}} {%endfor%}','array' => [1,2,3])
|
||||
assert_template_result('123','{%for item in array%}{{item}}{%endfor%}','array' => [1,2,3])
|
||||
assert_template_result('123','{% for item in array %}{{item}}{% endfor %}','array' => [1,2,3])
|
||||
assert_template_result('abcd','{%for item in array%}{{item}}{%endfor%}','array' => ['a','b','c','d'])
|
||||
assert_template_result('a b c','{%for item in array%}{{item}}{%endfor%}','array' => ['a',' ','b',' ','c'])
|
||||
assert_template_result('abc','{%for item in array%}{{item}}{%endfor%}','array' => ['a','','b','','c'])
|
||||
assert_template_result(' 1 2 3 ', '{%for item in array%} {{item}} {%endfor%}', 'array' => [1, 2, 3])
|
||||
assert_template_result('123', '{%for item in array%}{{item}}{%endfor%}', 'array' => [1, 2, 3])
|
||||
assert_template_result('123', '{% for item in array %}{{item}}{% endfor %}', 'array' => [1, 2, 3])
|
||||
assert_template_result('abcd', '{%for item in array%}{{item}}{%endfor%}', 'array' => ['a', 'b', 'c', 'd'])
|
||||
assert_template_result('a b c', '{%for item in array%}{{item}}{%endfor%}', 'array' => ['a', ' ', 'b', ' ', 'c'])
|
||||
assert_template_result('abc', '{%for item in array%}{{item}}{%endfor%}', 'array' => ['a', '', 'b', '', 'c'])
|
||||
end
|
||||
|
||||
def test_for_helpers
|
||||
assigns = {'array' => [1,2,3] }
|
||||
assigns = { 'array' => [1, 2, 3] }
|
||||
assert_template_result(' 1/3 2/3 3/3 ',
|
||||
'{%for item in array%} {{forloop.index}}/{{forloop.length}} {%endfor%}',
|
||||
assigns)
|
||||
'{%for item in array%} {{forloop.index}}/{{forloop.length}} {%endfor%}',
|
||||
assigns)
|
||||
assert_template_result(' 1 2 3 ', '{%for item in array%} {{forloop.index}} {%endfor%}', assigns)
|
||||
assert_template_result(' 0 1 2 ', '{%for item in array%} {{forloop.index0}} {%endfor%}', assigns)
|
||||
assert_template_result(' 2 1 0 ', '{%for item in array%} {{forloop.rindex0}} {%endfor%}', assigns)
|
||||
@@ -77,20 +83,20 @@ HERE
|
||||
end
|
||||
|
||||
def test_for_and_if
|
||||
assigns = {'array' => [1,2,3] }
|
||||
assigns = { 'array' => [1, 2, 3] }
|
||||
assert_template_result('+--',
|
||||
'{%for item in array%}{% if forloop.first %}+{% else %}-{% endif %}{%endfor%}',
|
||||
assigns)
|
||||
'{%for item in array%}{% if forloop.first %}+{% else %}-{% endif %}{%endfor%}',
|
||||
assigns)
|
||||
end
|
||||
|
||||
def test_for_else
|
||||
assert_template_result('+++', '{%for item in array%}+{%else%}-{%endfor%}', 'array'=>[1,2,3])
|
||||
assert_template_result('-', '{%for item in array%}+{%else%}-{%endfor%}', 'array'=>[])
|
||||
assert_template_result('-', '{%for item in array%}+{%else%}-{%endfor%}', 'array'=>nil)
|
||||
assert_template_result('+++', '{%for item in array%}+{%else%}-{%endfor%}', 'array' => [1, 2, 3])
|
||||
assert_template_result('-', '{%for item in array%}+{%else%}-{%endfor%}', 'array' => [])
|
||||
assert_template_result('-', '{%for item in array%}+{%else%}-{%endfor%}', 'array' => nil)
|
||||
end
|
||||
|
||||
def test_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] }
|
||||
assert_template_result('12', '{%for i in array limit:2 %}{{ i }}{%endfor%}', assigns)
|
||||
assert_template_result('1234', '{%for i in array limit:4 %}{{ i }}{%endfor%}', assigns)
|
||||
assert_template_result('3456', '{%for i in array limit:4 offset:2 %}{{ i }}{%endfor%}', assigns)
|
||||
@@ -98,7 +104,7 @@ HERE
|
||||
end
|
||||
|
||||
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['offset'] = 2
|
||||
|
||||
@@ -106,17 +112,17 @@ HERE
|
||||
end
|
||||
|
||||
def test_nested_for
|
||||
assigns = {'array' => [[1,2],[3,4],[5,6]] }
|
||||
assigns = { 'array' => [[1, 2], [3, 4], [5, 6]] }
|
||||
assert_template_result('123456', '{%for item in array%}{%for i in item%}{{ i }}{%endfor%}{%endfor%}', assigns)
|
||||
end
|
||||
|
||||
def test_offset_only
|
||||
assigns = {'array' => [1,2,3,4,5,6,7,8,9,0]}
|
||||
assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] }
|
||||
assert_template_result('890', '{%for i in array offset:7 %}{{ i }}{%endfor%}', assigns)
|
||||
end
|
||||
|
||||
def test_pause_resume
|
||||
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
|
||||
{%for i in array.items limit: 3 %}{{i}}{%endfor%}
|
||||
next
|
||||
@@ -131,11 +137,11 @@ HERE
|
||||
next
|
||||
789
|
||||
XPCTD
|
||||
assert_template_result(expected,markup,assigns)
|
||||
assert_template_result(expected, markup, assigns)
|
||||
end
|
||||
|
||||
def test_pause_resume_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
|
||||
{%for i in array.items limit:3 %}{{i}}{%endfor%}
|
||||
next
|
||||
@@ -150,11 +156,11 @@ HERE
|
||||
next
|
||||
7
|
||||
XPCTD
|
||||
assert_template_result(expected,markup,assigns)
|
||||
assert_template_result(expected, markup, assigns)
|
||||
end
|
||||
|
||||
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
|
||||
{%for i in array.items limit:3 %}{{i}}{%endfor%}
|
||||
next
|
||||
@@ -169,103 +175,102 @@ HERE
|
||||
next
|
||||
7890
|
||||
XPCTD
|
||||
assert_template_result(expected,markup,assigns)
|
||||
assert_template_result(expected, markup, assigns)
|
||||
end
|
||||
|
||||
|
||||
def test_pause_resume_BIG_offset
|
||||
assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,0]}}
|
||||
markup = %q({%for i in array.items limit:3 %}{{i}}{%endfor%}
|
||||
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } }
|
||||
markup = '{%for i in array.items limit:3 %}{{i}}{%endfor%}
|
||||
next
|
||||
{%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%}
|
||||
next
|
||||
{%for i in array.items offset:continue limit:3 offset:1000 %}{{i}}{%endfor%})
|
||||
expected = %q(123
|
||||
{%for i in array.items offset:continue limit:3 offset:1000 %}{{i}}{%endfor%}'
|
||||
expected = '123
|
||||
next
|
||||
456
|
||||
next
|
||||
)
|
||||
assert_template_result(expected,markup,assigns)
|
||||
'
|
||||
assert_template_result(expected, markup, assigns)
|
||||
end
|
||||
|
||||
def test_for_with_break
|
||||
assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,10]}}
|
||||
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] } }
|
||||
|
||||
markup = '{% for i in array.items %}{% break %}{% endfor %}'
|
||||
expected = ""
|
||||
assert_template_result(expected,markup,assigns)
|
||||
assert_template_result(expected, markup, assigns)
|
||||
|
||||
markup = '{% for i in array.items %}{{ i }}{% break %}{% endfor %}'
|
||||
expected = "1"
|
||||
assert_template_result(expected,markup,assigns)
|
||||
assert_template_result(expected, markup, assigns)
|
||||
|
||||
markup = '{% for i in array.items %}{% break %}{{ i }}{% endfor %}'
|
||||
expected = ""
|
||||
assert_template_result(expected,markup,assigns)
|
||||
assert_template_result(expected, markup, assigns)
|
||||
|
||||
markup = '{% for i in array.items %}{{ i }}{% if i > 3 %}{% break %}{% endif %}{% endfor %}'
|
||||
expected = "1234"
|
||||
assert_template_result(expected,markup,assigns)
|
||||
assert_template_result(expected, markup, assigns)
|
||||
|
||||
# tests to ensure it only breaks out of the local for loop
|
||||
# and not all of them.
|
||||
assigns = {'array' => [[1,2],[3,4],[5,6]] }
|
||||
markup = '{% for item in array %}' +
|
||||
'{% for i in item %}' +
|
||||
'{% if i == 1 %}' +
|
||||
'{% break %}' +
|
||||
'{% endif %}' +
|
||||
'{{ i }}' +
|
||||
'{% endfor %}' +
|
||||
assigns = { 'array' => [[1, 2], [3, 4], [5, 6]] }
|
||||
markup = '{% for item in array %}' \
|
||||
'{% for i in item %}' \
|
||||
'{% if i == 1 %}' \
|
||||
'{% break %}' \
|
||||
'{% endif %}' \
|
||||
'{{ i }}' \
|
||||
'{% endfor %}' \
|
||||
'{% endfor %}'
|
||||
expected = '3456'
|
||||
assert_template_result(expected, markup, assigns)
|
||||
|
||||
# test break does nothing when unreached
|
||||
assigns = {'array' => {'items' => [1,2,3,4,5]}}
|
||||
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5] } }
|
||||
markup = '{% for i in array.items %}{% if i == 9999 %}{% break %}{% endif %}{{ i }}{% endfor %}'
|
||||
expected = '12345'
|
||||
assert_template_result(expected, markup, assigns)
|
||||
end
|
||||
|
||||
def test_for_with_continue
|
||||
assigns = {'array' => {'items' => [1,2,3,4,5]}}
|
||||
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5] } }
|
||||
|
||||
markup = '{% for i in array.items %}{% continue %}{% endfor %}'
|
||||
expected = ""
|
||||
assert_template_result(expected,markup,assigns)
|
||||
assert_template_result(expected, markup, assigns)
|
||||
|
||||
markup = '{% for i in array.items %}{{ i }}{% continue %}{% endfor %}'
|
||||
expected = "12345"
|
||||
assert_template_result(expected,markup,assigns)
|
||||
assert_template_result(expected, markup, assigns)
|
||||
|
||||
markup = '{% for i in array.items %}{% continue %}{{ i }}{% endfor %}'
|
||||
expected = ""
|
||||
assert_template_result(expected,markup,assigns)
|
||||
assert_template_result(expected, markup, assigns)
|
||||
|
||||
markup = '{% for i in array.items %}{% if i > 3 %}{% continue %}{% endif %}{{ i }}{% endfor %}'
|
||||
expected = "123"
|
||||
assert_template_result(expected,markup,assigns)
|
||||
assert_template_result(expected, markup, assigns)
|
||||
|
||||
markup = '{% for i in array.items %}{% if i == 3 %}{% continue %}{% else %}{{ i }}{% endif %}{% endfor %}'
|
||||
expected = "1245"
|
||||
assert_template_result(expected,markup,assigns)
|
||||
assert_template_result(expected, markup, assigns)
|
||||
|
||||
# tests to ensure it only continues the local for loop and not all of them.
|
||||
assigns = {'array' => [[1,2],[3,4],[5,6]] }
|
||||
markup = '{% for item in array %}' +
|
||||
'{% for i in item %}' +
|
||||
'{% if i == 1 %}' +
|
||||
'{% continue %}' +
|
||||
'{% endif %}' +
|
||||
'{{ i }}' +
|
||||
'{% endfor %}' +
|
||||
assigns = { 'array' => [[1, 2], [3, 4], [5, 6]] }
|
||||
markup = '{% for item in array %}' \
|
||||
'{% for i in item %}' \
|
||||
'{% if i == 1 %}' \
|
||||
'{% continue %}' \
|
||||
'{% endif %}' \
|
||||
'{{ i }}' \
|
||||
'{% endfor %}' \
|
||||
'{% endfor %}'
|
||||
expected = '23456'
|
||||
assert_template_result(expected, markup, assigns)
|
||||
|
||||
# test continue does nothing when unreached
|
||||
assigns = {'array' => {'items' => [1,2,3,4,5]}}
|
||||
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5] } }
|
||||
markup = '{% for i in array.items %}{% if i == 9999 %}{% continue %}{% endif %}{{ i }}{% endfor %}'
|
||||
expected = '12345'
|
||||
assert_template_result(expected, markup, assigns)
|
||||
@@ -277,25 +282,45 @@ HERE
|
||||
# the functionality for backwards compatibility
|
||||
|
||||
assert_template_result('test string',
|
||||
'{%for val in string%}{{val}}{%endfor%}',
|
||||
'string' => "test string")
|
||||
'{%for val in string%}{{val}}{%endfor%}',
|
||||
'string' => "test string")
|
||||
|
||||
assert_template_result('test string',
|
||||
'{%for val in string limit:1%}{{val}}{%endfor%}',
|
||||
'string' => "test string")
|
||||
'{%for val in string limit:1%}{{val}}{%endfor%}',
|
||||
'string' => "test string")
|
||||
|
||||
assert_template_result('val-string-1-1-0-1-0-true-true-test string',
|
||||
'{%for val in string%}' +
|
||||
'{{forloop.name}}-' +
|
||||
'{{forloop.index}}-' +
|
||||
'{{forloop.length}}-' +
|
||||
'{{forloop.index0}}-' +
|
||||
'{{forloop.rindex}}-' +
|
||||
'{{forloop.rindex0}}-' +
|
||||
'{{forloop.first}}-' +
|
||||
'{{forloop.last}}-' +
|
||||
'{{val}}{%endfor%}',
|
||||
'string' => "test string")
|
||||
'{%for val in string%}' \
|
||||
'{{forloop.name}}-' \
|
||||
'{{forloop.index}}-' \
|
||||
'{{forloop.length}}-' \
|
||||
'{{forloop.index0}}-' \
|
||||
'{{forloop.rindex}}-' \
|
||||
'{{forloop.rindex0}}-' \
|
||||
'{{forloop.first}}-' \
|
||||
'{{forloop.last}}-' \
|
||||
'{{val}}{%endfor%}',
|
||||
'string' => "test string")
|
||||
end
|
||||
|
||||
def test_for_parentloop_references_parent_loop
|
||||
assert_template_result('1.1 1.2 1.3 2.1 2.2 2.3 ',
|
||||
'{% for inner in outer %}{% for k in inner %}' \
|
||||
'{{ forloop.parentloop.index }}.{{ forloop.index }} ' \
|
||||
'{% endfor %}{% endfor %}',
|
||||
'outer' => [[1, 1, 1], [1, 1, 1]])
|
||||
end
|
||||
|
||||
def test_for_parentloop_nil_when_not_present
|
||||
assert_template_result('.1 .2 ',
|
||||
'{% for inner in outer %}' \
|
||||
'{{ forloop.parentloop.index }}.{{ forloop.index }} ' \
|
||||
'{% endfor %}',
|
||||
'outer' => [[1, 1, 1], [1, 1, 1]])
|
||||
end
|
||||
|
||||
def test_inner_for_over_empty_input
|
||||
assert_template_result 'oo', '{% for a in (1..2) %}o{% for b in empty %}{% endfor %}{% endfor %}'
|
||||
end
|
||||
|
||||
def test_blank_string_not_iterable
|
||||
@@ -303,7 +328,7 @@ HERE
|
||||
end
|
||||
|
||||
def test_bad_variable_naming_in_for_loop
|
||||
assert_raise(Liquid::SyntaxError) do
|
||||
assert_raises(Liquid::SyntaxError) do
|
||||
Liquid::Template.parse('{% for a/b in x %}{% endfor %}')
|
||||
end
|
||||
end
|
||||
@@ -311,7 +336,7 @@ HERE
|
||||
def test_spacing_with_variable_naming_in_for_loop
|
||||
expected = '12345'
|
||||
template = '{% for item in items %}{{item}}{% endfor %}'
|
||||
assigns = {'items' => [1,2,3,4,5]}
|
||||
assigns = { 'items' => [1, 2, 3, 4, 5] }
|
||||
assert_template_result(expected, template, assigns)
|
||||
end
|
||||
|
||||
@@ -329,13 +354,13 @@ HERE
|
||||
|
||||
def load_slice(from, to)
|
||||
@load_slice_called = true
|
||||
@data[(from..to-1)]
|
||||
@data[(from..to - 1)]
|
||||
end
|
||||
end
|
||||
|
||||
def test_iterate_with_each_when_no_limit_applied
|
||||
loader = LoaderDrop.new([1,2,3,4,5])
|
||||
assigns = {'items' => loader}
|
||||
loader = LoaderDrop.new([1, 2, 3, 4, 5])
|
||||
assigns = { 'items' => loader }
|
||||
expected = '12345'
|
||||
template = '{% for item in items %}{{item}}{% endfor %}'
|
||||
assert_template_result(expected, template, assigns)
|
||||
@@ -344,8 +369,8 @@ HERE
|
||||
end
|
||||
|
||||
def test_iterate_with_load_slice_when_limit_applied
|
||||
loader = LoaderDrop.new([1,2,3,4,5])
|
||||
assigns = {'items' => loader}
|
||||
loader = LoaderDrop.new([1, 2, 3, 4, 5])
|
||||
assigns = { 'items' => loader }
|
||||
expected = '1'
|
||||
template = '{% for item in items limit:1 %}{{item}}{% endfor %}'
|
||||
assert_template_result(expected, template, assigns)
|
||||
@@ -354,8 +379,8 @@ HERE
|
||||
end
|
||||
|
||||
def test_iterate_with_load_slice_when_limit_and_offset_applied
|
||||
loader = LoaderDrop.new([1,2,3,4,5])
|
||||
assigns = {'items' => loader}
|
||||
loader = LoaderDrop.new([1, 2, 3, 4, 5])
|
||||
assigns = { 'items' => loader }
|
||||
expected = '34'
|
||||
template = '{% for item in items offset:2 limit:2 %}{{item}}{% endfor %}'
|
||||
assert_template_result(expected, template, assigns)
|
||||
@@ -364,12 +389,22 @@ HERE
|
||||
end
|
||||
|
||||
def test_iterate_with_load_slice_returns_same_results_as_without
|
||||
loader = LoaderDrop.new([1,2,3,4,5])
|
||||
loader_assigns = {'items' => loader}
|
||||
array_assigns = {'items' => [1,2,3,4,5]}
|
||||
loader = LoaderDrop.new([1, 2, 3, 4, 5])
|
||||
loader_assigns = { 'items' => loader }
|
||||
array_assigns = { 'items' => [1, 2, 3, 4, 5] }
|
||||
expected = '34'
|
||||
template = '{% for item in items offset:2 limit:2 %}{{item}}{% endfor %}'
|
||||
assert_template_result(expected, template, loader_assigns)
|
||||
assert_template_result(expected, template, array_assigns)
|
||||
end
|
||||
|
||||
def test_for_cleans_up_registers
|
||||
context = Context.new(ErrorDrop.new)
|
||||
|
||||
assert_raises(StandardError) do
|
||||
Liquid::Template.parse('{% for i in (1..2) %}{{ standard_error }}{% endfor %}').render!(context)
|
||||
end
|
||||
|
||||
assert context.registers[:for_stack].empty?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,105 +1,103 @@
|
||||
require 'test_helper'
|
||||
|
||||
class IfElseTagTest < Test::Unit::TestCase
|
||||
class IfElseTagTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_if
|
||||
assert_template_result(' ',' {% if false %} this text should not go into the output {% endif %} ')
|
||||
assert_template_result(' ', ' {% if false %} this text should not go into the output {% endif %} ')
|
||||
assert_template_result(' this text should go into the output ',
|
||||
' {% if true %} this text should go into the output {% endif %} ')
|
||||
assert_template_result(' you rock ?','{% if false %} you suck {% endif %} {% if true %} you rock {% endif %}?')
|
||||
' {% if true %} this text should go into the output {% endif %} ')
|
||||
assert_template_result(' you rock ?', '{% if false %} you suck {% endif %} {% if true %} you rock {% endif %}?')
|
||||
end
|
||||
|
||||
def test_literal_comparisons
|
||||
assert_template_result(' NO ', '{% assign v = false %}{% if v %} YES {% else %} NO {% endif %}')
|
||||
assert_template_result(' YES ', '{% assign v = nil %}{% if v == nil %} YES {% else %} NO {% endif %}')
|
||||
end
|
||||
|
||||
def test_if_else
|
||||
assert_template_result(' YES ','{% if false %} NO {% else %} YES {% endif %}')
|
||||
assert_template_result(' YES ','{% if true %} YES {% else %} NO {% endif %}')
|
||||
assert_template_result(' YES ','{% if "foo" %} YES {% else %} NO {% endif %}')
|
||||
assert_template_result(' YES ', '{% if false %} NO {% else %} YES {% endif %}')
|
||||
assert_template_result(' YES ', '{% if true %} YES {% else %} NO {% endif %}')
|
||||
assert_template_result(' YES ', '{% if "foo" %} YES {% else %} NO {% endif %}')
|
||||
end
|
||||
|
||||
def test_if_boolean
|
||||
assert_template_result(' YES ','{% if var %} YES {% endif %}', 'var' => true)
|
||||
assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => true)
|
||||
end
|
||||
|
||||
def test_if_or
|
||||
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' => false, 'b' => true)
|
||||
assert_template_result('', '{% if a or b %} YES {% endif %}', 'a' => false, 'b' => false)
|
||||
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' => false, 'b' => true)
|
||||
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('', '{% if a or b or c %} YES {% endif %}', 'a' => false, 'b' => false, 'c' => false)
|
||||
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)
|
||||
end
|
||||
|
||||
def test_if_or_with_operators
|
||||
assert_template_result(' YES ','{% if a == true or b == true %} YES {% endif %}', 'a' => true, 'b' => true)
|
||||
assert_template_result(' YES ','{% if a == true or b == false %} YES {% endif %}', 'a' => true, 'b' => true)
|
||||
assert_template_result('','{% if a == false or b == false %} YES {% endif %}', 'a' => true, 'b' => true)
|
||||
assert_template_result(' YES ', '{% if a == true or b == true %} YES {% endif %}', 'a' => true, 'b' => true)
|
||||
assert_template_result(' YES ', '{% if a == true or b == false %} YES {% endif %}', 'a' => true, 'b' => true)
|
||||
assert_template_result('', '{% if a == false or b == false %} YES {% endif %}', 'a' => true, 'b' => true)
|
||||
end
|
||||
|
||||
def test_comparison_of_strings_containing_and_or_or
|
||||
assert_nothing_raised do
|
||||
awful_markup = "a == 'and' and b == 'or' and c == 'foo and bar' and d == 'bar or baz' and e == 'foo' and foo and bar"
|
||||
assigns = {'a' => 'and', 'b' => 'or', 'c' => 'foo and bar', 'd' => 'bar or baz', 'e' => 'foo', 'foo' => true, 'bar' => true}
|
||||
assert_template_result(' YES ',"{% if #{awful_markup} %} YES {% endif %}", assigns)
|
||||
end
|
||||
awful_markup = "a == 'and' and b == 'or' and c == 'foo and bar' and d == 'bar or baz' and e == 'foo' and foo and bar"
|
||||
assigns = { 'a' => 'and', 'b' => 'or', 'c' => 'foo and bar', 'd' => 'bar or baz', 'e' => 'foo', 'foo' => true, 'bar' => true }
|
||||
assert_template_result(' YES ', "{% if #{awful_markup} %} YES {% endif %}", assigns)
|
||||
end
|
||||
|
||||
def test_comparison_of_expressions_starting_with_and_or_or
|
||||
assigns = {'order' => {'items_count' => 0}, 'android' => {'name' => 'Roy'}}
|
||||
assert_nothing_raised do
|
||||
assert_template_result( "YES",
|
||||
"{% if android.name == 'Roy' %}YES{% endif %}",
|
||||
assigns)
|
||||
end
|
||||
assert_nothing_raised do
|
||||
assert_template_result( "YES",
|
||||
"{% if order.items_count == 0 %}YES{% endif %}",
|
||||
assigns)
|
||||
end
|
||||
assigns = { 'order' => { 'items_count' => 0 }, 'android' => { 'name' => 'Roy' } }
|
||||
assert_template_result("YES",
|
||||
"{% if android.name == 'Roy' %}YES{% endif %}",
|
||||
assigns)
|
||||
assert_template_result("YES",
|
||||
"{% if order.items_count == 0 %}YES{% endif %}",
|
||||
assigns)
|
||||
end
|
||||
|
||||
def test_if_and
|
||||
assert_template_result(' YES ','{% if true and true %} YES {% endif %}')
|
||||
assert_template_result('','{% if false and true %} YES {% endif %}')
|
||||
assert_template_result('','{% if false and true %} YES {% endif %}')
|
||||
assert_template_result(' YES ', '{% if true and true %} YES {% endif %}')
|
||||
assert_template_result('', '{% if false and true %} YES {% endif %}')
|
||||
assert_template_result('', '{% if false and true %} YES {% endif %}')
|
||||
end
|
||||
|
||||
|
||||
def test_hash_miss_generates_false
|
||||
assert_template_result('','{% if foo.bar %} NO {% endif %}', 'foo' => {})
|
||||
assert_template_result('', '{% if foo.bar %} NO {% endif %}', 'foo' => {})
|
||||
end
|
||||
|
||||
def test_if_from_variable
|
||||
assert_template_result('','{% if var %} NO {% endif %}', 'var' => false)
|
||||
assert_template_result('','{% if var %} NO {% endif %}', 'var' => nil)
|
||||
assert_template_result('','{% if foo.bar %} NO {% endif %}', 'foo' => {'bar' => false})
|
||||
assert_template_result('','{% if foo.bar %} NO {% endif %}', 'foo' => {})
|
||||
assert_template_result('','{% if foo.bar %} NO {% endif %}', 'foo' => nil)
|
||||
assert_template_result('','{% if foo.bar %} NO {% endif %}', 'foo' => true)
|
||||
assert_template_result('', '{% if var %} NO {% endif %}', 'var' => false)
|
||||
assert_template_result('', '{% if var %} NO {% endif %}', 'var' => nil)
|
||||
assert_template_result('', '{% if foo.bar %} NO {% endif %}', 'foo' => { 'bar' => false })
|
||||
assert_template_result('', '{% if foo.bar %} NO {% endif %}', 'foo' => {})
|
||||
assert_template_result('', '{% if foo.bar %} NO {% endif %}', 'foo' => nil)
|
||||
assert_template_result('', '{% if foo.bar %} NO {% endif %}', 'foo' => true)
|
||||
|
||||
assert_template_result(' YES ','{% if var %} YES {% endif %}', 'var' => "text")
|
||||
assert_template_result(' YES ','{% if var %} YES {% endif %}', 'var' => true)
|
||||
assert_template_result(' YES ','{% if var %} YES {% endif %}', 'var' => 1)
|
||||
assert_template_result(' YES ','{% if var %} YES {% endif %}', 'var' => {})
|
||||
assert_template_result(' YES ','{% if var %} YES {% endif %}', 'var' => [])
|
||||
assert_template_result(' YES ','{% if "foo" %} YES {% endif %}')
|
||||
assert_template_result(' YES ','{% if foo.bar %} YES {% endif %}', 'foo' => {'bar' => true})
|
||||
assert_template_result(' YES ','{% if foo.bar %} YES {% endif %}', 'foo' => {'bar' => "text"})
|
||||
assert_template_result(' YES ','{% if foo.bar %} YES {% endif %}', 'foo' => {'bar' => 1 })
|
||||
assert_template_result(' YES ','{% if foo.bar %} YES {% endif %}', 'foo' => {'bar' => {} })
|
||||
assert_template_result(' YES ','{% if foo.bar %} YES {% endif %}', 'foo' => {'bar' => [] })
|
||||
assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => "text")
|
||||
assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => true)
|
||||
assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => 1)
|
||||
assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => {})
|
||||
assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => [])
|
||||
assert_template_result(' YES ', '{% if "foo" %} YES {% endif %}')
|
||||
assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => true })
|
||||
assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => "text" })
|
||||
assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => 1 })
|
||||
assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => {} })
|
||||
assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => [] })
|
||||
|
||||
assert_template_result(' YES ','{% if var %} NO {% else %} YES {% endif %}', 'var' => false)
|
||||
assert_template_result(' YES ','{% if var %} NO {% else %} YES {% endif %}', 'var' => nil)
|
||||
assert_template_result(' YES ','{% if var %} YES {% else %} NO {% endif %}', 'var' => true)
|
||||
assert_template_result(' YES ','{% if "foo" %} YES {% else %} NO {% endif %}', 'var' => "text")
|
||||
assert_template_result(' YES ', '{% if var %} NO {% else %} YES {% endif %}', 'var' => false)
|
||||
assert_template_result(' YES ', '{% if var %} NO {% else %} YES {% endif %}', 'var' => nil)
|
||||
assert_template_result(' YES ', '{% if var %} YES {% else %} NO {% endif %}', 'var' => true)
|
||||
assert_template_result(' YES ', '{% if "foo" %} YES {% else %} NO {% endif %}', 'var' => "text")
|
||||
|
||||
assert_template_result(' YES ','{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => {'bar' => false})
|
||||
assert_template_result(' YES ','{% if foo.bar %} YES {% else %} NO {% endif %}', 'foo' => {'bar' => true})
|
||||
assert_template_result(' YES ','{% if foo.bar %} YES {% else %} NO {% endif %}', 'foo' => {'bar' => "text"})
|
||||
assert_template_result(' YES ','{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => {'notbar' => true})
|
||||
assert_template_result(' YES ','{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => {})
|
||||
assert_template_result(' YES ','{% if foo.bar %} NO {% else %} YES {% endif %}', 'notfoo' => {'bar' => true})
|
||||
assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => { 'bar' => false })
|
||||
assert_template_result(' YES ', '{% if foo.bar %} YES {% else %} NO {% endif %}', 'foo' => { 'bar' => true })
|
||||
assert_template_result(' YES ', '{% if foo.bar %} YES {% else %} NO {% endif %}', 'foo' => { 'bar' => "text" })
|
||||
assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => { 'notbar' => true })
|
||||
assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => {})
|
||||
assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', 'notfoo' => { 'bar' => true })
|
||||
end
|
||||
|
||||
def test_nested_if
|
||||
@@ -111,56 +109,80 @@ class IfElseTagTest < Test::Unit::TestCase
|
||||
assert_template_result(' YES ', '{% if true %}{% if true %} YES {% else %} NO {% endif %}{% else %} NO {% endif %}')
|
||||
assert_template_result(' YES ', '{% if true %}{% if false %} NO {% else %} YES {% endif %}{% else %} NO {% endif %}')
|
||||
assert_template_result(' YES ', '{% if false %}{% if true %} NO {% else %} NONO {% endif %}{% else %} YES {% endif %}')
|
||||
|
||||
end
|
||||
|
||||
def test_comparisons_on_null
|
||||
assert_template_result('','{% if null < 10 %} NO {% endif %}')
|
||||
assert_template_result('','{% if null <= 10 %} NO {% endif %}')
|
||||
assert_template_result('','{% if null >= 10 %} NO {% endif %}')
|
||||
assert_template_result('','{% if null > 10 %} NO {% endif %}')
|
||||
assert_template_result('', '{% if null < 10 %} NO {% endif %}')
|
||||
assert_template_result('', '{% if null <= 10 %} NO {% endif %}')
|
||||
assert_template_result('', '{% if null >= 10 %} NO {% endif %}')
|
||||
assert_template_result('', '{% if null > 10 %} NO {% endif %}')
|
||||
|
||||
assert_template_result('','{% if 10 < null %} NO {% endif %}')
|
||||
assert_template_result('','{% if 10 <= null %} NO {% endif %}')
|
||||
assert_template_result('','{% if 10 >= null %} NO {% endif %}')
|
||||
assert_template_result('','{% if 10 > null %} NO {% endif %}')
|
||||
assert_template_result('', '{% if 10 < null %} NO {% endif %}')
|
||||
assert_template_result('', '{% if 10 <= null %} NO {% endif %}')
|
||||
assert_template_result('', '{% if 10 >= null %} NO {% endif %}')
|
||||
assert_template_result('', '{% if 10 > null %} NO {% endif %}')
|
||||
end
|
||||
|
||||
def test_else_if
|
||||
assert_template_result('0','{% if 0 == 0 %}0{% elsif 1 == 1%}1{% else %}2{% endif %}')
|
||||
assert_template_result('1','{% if 0 != 0 %}0{% elsif 1 == 1%}1{% else %}2{% endif %}')
|
||||
assert_template_result('2','{% if 0 != 0 %}0{% elsif 1 != 1%}1{% else %}2{% endif %}')
|
||||
assert_template_result('0', '{% if 0 == 0 %}0{% elsif 1 == 1%}1{% else %}2{% endif %}')
|
||||
assert_template_result('1', '{% if 0 != 0 %}0{% elsif 1 == 1%}1{% else %}2{% endif %}')
|
||||
assert_template_result('2', '{% if 0 != 0 %}0{% elsif 1 != 1%}1{% else %}2{% endif %}')
|
||||
|
||||
assert_template_result('elsif','{% if false %}if{% elsif true %}elsif{% endif %}')
|
||||
assert_template_result('elsif', '{% if false %}if{% elsif true %}elsif{% endif %}')
|
||||
end
|
||||
|
||||
def test_syntax_error_no_variable
|
||||
assert_raise(SyntaxError){ assert_template_result('', '{% if jerry == 1 %}')}
|
||||
assert_raises(SyntaxError){ assert_template_result('', '{% if jerry == 1 %}') }
|
||||
end
|
||||
|
||||
def test_syntax_error_no_expression
|
||||
assert_raise(SyntaxError) { assert_template_result('', '{% if %}') }
|
||||
assert_raises(SyntaxError) { assert_template_result('', '{% if %}') }
|
||||
end
|
||||
|
||||
def test_if_with_custom_condition
|
||||
original_op = Condition.operators['contains']
|
||||
Condition.operators['contains'] = :[]
|
||||
|
||||
assert_template_result('yes', %({% if 'bob' contains 'o' %}yes{% endif %}))
|
||||
assert_template_result('no', %({% if 'bob' contains 'f' %}yes{% else %}no{% endif %}))
|
||||
ensure
|
||||
Condition.operators.delete 'contains'
|
||||
Condition.operators['contains'] = original_op
|
||||
end
|
||||
|
||||
def test_operators_are_ignored_unless_isolated
|
||||
original_op = Condition.operators['contains']
|
||||
Condition.operators['contains'] = :[]
|
||||
|
||||
assert_template_result('yes',
|
||||
%({% if 'gnomeslab-and-or-liquid' contains 'gnomeslab-and-or-liquid' %}yes{% endif %}))
|
||||
%({% if 'gnomeslab-and-or-liquid' contains 'gnomeslab-and-or-liquid' %}yes{% endif %}))
|
||||
ensure
|
||||
Condition.operators['contains'] = original_op
|
||||
end
|
||||
|
||||
def test_operators_are_whitelisted
|
||||
assert_raise(SyntaxError) do
|
||||
assert_raises(SyntaxError) do
|
||||
assert_template_result('', %({% if 1 or throw or or 1 %}yes{% endif %}))
|
||||
end
|
||||
end
|
||||
|
||||
def test_multiple_conditions
|
||||
tpl = "{% if a or b and c %}true{% else %}false{% endif %}"
|
||||
|
||||
tests = {
|
||||
[true, true, true] => true,
|
||||
[true, true, false] => true,
|
||||
[true, false, true] => true,
|
||||
[true, false, false] => true,
|
||||
[false, true, true] => true,
|
||||
[false, true, false] => false,
|
||||
[false, false, true] => false,
|
||||
[false, false, false] => false,
|
||||
}
|
||||
|
||||
tests.each do |vals, expected|
|
||||
a, b, c = vals
|
||||
assigns = { 'a' => a, 'b' => b, 'c' => c }
|
||||
assert_template_result expected.to_s, tpl, assigns, assigns.to_s
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
require 'test_helper'
|
||||
|
||||
class TestFileSystem
|
||||
def read_template_file(template_path, context)
|
||||
def read_template_file(template_path)
|
||||
case template_path
|
||||
when "product"
|
||||
"Product: {{ product.title }} "
|
||||
@@ -27,6 +27,9 @@ class TestFileSystem
|
||||
when "pick_a_source"
|
||||
"from TestFileSystem"
|
||||
|
||||
when 'assignments'
|
||||
"{% assign foo = 'bar' %}"
|
||||
|
||||
else
|
||||
template_path
|
||||
end
|
||||
@@ -34,14 +37,14 @@ class TestFileSystem
|
||||
end
|
||||
|
||||
class OtherFileSystem
|
||||
def read_template_file(template_path, context)
|
||||
def read_template_file(template_path)
|
||||
'from OtherFileSystem'
|
||||
end
|
||||
end
|
||||
|
||||
class CountingFileSystem
|
||||
attr_reader :count
|
||||
def read_template_file(template_path, context)
|
||||
def read_template_file(template_path)
|
||||
@count ||= 0
|
||||
@count += 1
|
||||
'from CountingFileSystem'
|
||||
@@ -65,7 +68,7 @@ class CustomInclude < Liquid::Tag
|
||||
end
|
||||
end
|
||||
|
||||
class IncludeTagTest < Test::Unit::TestCase
|
||||
class IncludeTagTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def setup
|
||||
@@ -74,23 +77,22 @@ class IncludeTagTest < Test::Unit::TestCase
|
||||
|
||||
def test_include_tag_looks_for_file_system_in_registers_first
|
||||
assert_equal 'from OtherFileSystem',
|
||||
Template.parse("{% include 'pick_a_source' %}").render!({}, :registers => {:file_system => OtherFileSystem.new})
|
||||
Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: OtherFileSystem.new })
|
||||
end
|
||||
|
||||
|
||||
def test_include_tag_with
|
||||
assert_template_result "Product: Draft 151cm ",
|
||||
"{% include 'product' with products[0] %}", "products" => [ {'title' => 'Draft 151cm'}, {'title' => 'Element 155cm'} ]
|
||||
"{% include 'product' with products[0] %}", "products" => [ { 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' } ]
|
||||
end
|
||||
|
||||
def test_include_tag_with_default_name
|
||||
assert_template_result "Product: Draft 151cm ",
|
||||
"{% include 'product' %}", "product" => {'title' => 'Draft 151cm'}
|
||||
"{% include 'product' %}", "product" => { 'title' => 'Draft 151cm' }
|
||||
end
|
||||
|
||||
def test_include_tag_for
|
||||
assert_template_result "Product: Draft 151cm Product: Element 155cm ",
|
||||
"{% include 'product' for products %}", "products" => [ {'title' => 'Draft 151cm'}, {'title' => 'Element 155cm'} ]
|
||||
"{% include 'product' for products %}", "products" => [ { 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' } ]
|
||||
end
|
||||
|
||||
def test_include_tag_with_local_variables
|
||||
@@ -105,7 +107,11 @@ class IncludeTagTest < Test::Unit::TestCase
|
||||
def test_include_tag_with_multiple_local_variables_from_context
|
||||
assert_template_result "Locale: test123 test321",
|
||||
"{% include 'locale_variables' echo1: echo1, echo2: more_echos.echo2 %}",
|
||||
'echo1' => 'test123', 'more_echos' => { "echo2" => 'test321'}
|
||||
'echo1' => 'test123', 'more_echos' => { "echo2" => 'test321' }
|
||||
end
|
||||
|
||||
def test_included_templates_assigns_variables
|
||||
assert_template_result "bar", "{% include 'assignments' %}{{ foo }}"
|
||||
end
|
||||
|
||||
def test_nested_include_tag
|
||||
@@ -116,38 +122,24 @@ class IncludeTagTest < Test::Unit::TestCase
|
||||
|
||||
def test_nested_include_with_variable
|
||||
assert_template_result "Product: Draft 151cm details ",
|
||||
"{% include 'nested_product_template' with product %}", "product" => {"title" => 'Draft 151cm'}
|
||||
"{% include 'nested_product_template' with product %}", "product" => { "title" => 'Draft 151cm' }
|
||||
|
||||
assert_template_result "Product: Draft 151cm details Product: Element 155cm details ",
|
||||
"{% include 'nested_product_template' for products %}", "products" => [{"title" => 'Draft 151cm'}, {"title" => 'Element 155cm'}]
|
||||
"{% include 'nested_product_template' for products %}", "products" => [{ "title" => 'Draft 151cm' }, { "title" => 'Element 155cm' }]
|
||||
end
|
||||
|
||||
def test_recursively_included_template_does_not_produce_endless_loop
|
||||
|
||||
infinite_file_system = Class.new do
|
||||
def read_template_file(template_path, context)
|
||||
def read_template_file(template_path)
|
||||
"-{% include 'loop' %}"
|
||||
end
|
||||
end
|
||||
|
||||
Liquid::Template.file_system = infinite_file_system.new
|
||||
|
||||
assert_raise(Liquid::StackLevelError) do
|
||||
assert_raises(Liquid::StackLevelError, SystemStackError) do
|
||||
Template.parse("{% include 'loop' %}").render!
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def test_backwards_compatability_support_for_overridden_read_template_file
|
||||
infinite_file_system = Class.new do
|
||||
def read_template_file(template_path) # testing only one argument here.
|
||||
"- hi mom"
|
||||
end
|
||||
end
|
||||
|
||||
Liquid::Template.file_system = infinite_file_system.new
|
||||
|
||||
Template.parse("{% include 'hi_mom' %}").render!
|
||||
end
|
||||
|
||||
def test_dynamically_choosen_template
|
||||
@@ -155,24 +147,24 @@ class IncludeTagTest < Test::Unit::TestCase
|
||||
assert_template_result "Test321", "{% include template %}", "template" => 'Test321'
|
||||
|
||||
assert_template_result "Product: Draft 151cm ", "{% include template for product %}",
|
||||
"template" => 'product', 'product' => { 'title' => 'Draft 151cm'}
|
||||
"template" => 'product', 'product' => { 'title' => 'Draft 151cm' }
|
||||
end
|
||||
|
||||
def test_include_tag_caches_second_read_of_same_partial
|
||||
file_system = CountingFileSystem.new
|
||||
assert_equal 'from CountingFileSystemfrom CountingFileSystem',
|
||||
Template.parse("{% include 'pick_a_source' %}{% include 'pick_a_source' %}").render!({}, :registers => {:file_system => file_system})
|
||||
Template.parse("{% include 'pick_a_source' %}{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system })
|
||||
assert_equal 1, file_system.count
|
||||
end
|
||||
|
||||
def test_include_tag_doesnt_cache_partials_across_renders
|
||||
file_system = CountingFileSystem.new
|
||||
assert_equal 'from CountingFileSystem',
|
||||
Template.parse("{% include 'pick_a_source' %}").render!({}, :registers => {:file_system => file_system})
|
||||
Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system })
|
||||
assert_equal 1, file_system.count
|
||||
|
||||
assert_equal 'from CountingFileSystem',
|
||||
Template.parse("{% include 'pick_a_source' %}").render!({}, :registers => {:file_system => file_system})
|
||||
Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system })
|
||||
assert_equal 2, file_system.count
|
||||
end
|
||||
|
||||
@@ -209,4 +201,27 @@ class IncludeTagTest < Test::Unit::TestCase
|
||||
a.render!
|
||||
assert_empty a.errors
|
||||
end
|
||||
|
||||
def test_passing_options_to_included_templates
|
||||
assert_raises(Liquid::SyntaxError) do
|
||||
Template.parse("{% include template %}", error_mode: :strict).render!("template" => '{{ "X" || downcase }}')
|
||||
end
|
||||
with_error_mode(:lax) do
|
||||
assert_equal 'x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: true).render!("template" => '{{ "X" || downcase }}')
|
||||
end
|
||||
assert_raises(Liquid::SyntaxError) do
|
||||
Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:locale]).render!("template" => '{{ "X" || downcase }}')
|
||||
end
|
||||
with_error_mode(:lax) do
|
||||
assert_equal 'x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:error_mode]).render!("template" => '{{ "X" || downcase }}')
|
||||
end
|
||||
end
|
||||
|
||||
def test_including_via_variable_value
|
||||
assert_template_result "from TestFileSystem", "{% assign page = 'pick_a_source' %}{% include page %}"
|
||||
|
||||
assert_template_result "Product: Draft 151cm ", "{% assign page = 'product' %}{% include page %}", "product" => { 'title' => 'Draft 151cm' }
|
||||
|
||||
assert_template_result "Product: Draft 151cm ", "{% assign page = 'product' %}{% include page for foo %}", "foo" => { 'title' => 'Draft 151cm' }
|
||||
end
|
||||
end # IncludeTagTest
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
require 'test_helper'
|
||||
|
||||
class IncrementTagTest < Test::Unit::TestCase
|
||||
class IncrementTagTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_inc
|
||||
assert_template_result('0','{%increment port %}', {})
|
||||
assert_template_result('0 1','{%increment port %} {%increment port%}', {})
|
||||
assert_template_result('0', '{%increment port %}', {})
|
||||
assert_template_result('0 1', '{%increment port %} {%increment port%}', {})
|
||||
assert_template_result('0 0 1 2 1',
|
||||
'{%increment port %} {%increment starboard%} ' +
|
||||
'{%increment port %} {%increment port%} ' +
|
||||
'{%increment starboard %}', {})
|
||||
'{%increment port %} {%increment starboard%} ' \
|
||||
'{%increment port %} {%increment port%} ' \
|
||||
'{%increment starboard %}', {})
|
||||
end
|
||||
|
||||
def test_dec
|
||||
assert_template_result('9','{%decrement port %}', { 'port' => 10})
|
||||
assert_template_result('-1 -2','{%decrement port %} {%decrement port%}', {})
|
||||
assert_template_result('9', '{%decrement port %}', { 'port' => 10 })
|
||||
assert_template_result('-1 -2', '{%decrement port %} {%decrement port%}', {})
|
||||
assert_template_result('1 5 2 2 5',
|
||||
'{%increment port %} {%increment starboard%} ' +
|
||||
'{%increment port %} {%decrement port%} ' +
|
||||
'{%decrement starboard %}', { 'port' => 1, 'starboard' => 5 })
|
||||
'{%increment port %} {%increment starboard%} ' \
|
||||
'{%increment port %} {%decrement port%} ' \
|
||||
'{%decrement starboard %}', { 'port' => 1, 'starboard' => 5 })
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
require 'test_helper'
|
||||
|
||||
class RawTagTest < Test::Unit::TestCase
|
||||
class RawTagTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_tag_in_raw
|
||||
assert_template_result '{% comment %} test {% endcomment %}',
|
||||
'{% raw %}{% comment %} test {% endcomment %}{% endraw %}'
|
||||
'{% raw %}{% comment %} test {% endcomment %}{% endraw %}'
|
||||
end
|
||||
|
||||
def test_output_in_raw
|
||||
@@ -22,4 +22,10 @@ class RawTagTest < Test::Unit::TestCase
|
||||
assert_template_result ' test {% raw %} {% endraw %}', '{% raw %} test {% raw %} {% {% endraw %}endraw %}'
|
||||
assert_template_result ' Foobar {{ invalid 1', '{% raw %} Foobar {{ invalid {% endraw %}{{ 1 }}'
|
||||
end
|
||||
|
||||
def test_invalid_raw
|
||||
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 %}')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,120 +1,120 @@
|
||||
require 'test_helper'
|
||||
|
||||
class StandardTagTest < Test::Unit::TestCase
|
||||
class StandardTagTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_no_transform
|
||||
assert_template_result('this text should come out of the template without change...',
|
||||
'this text should come out of the template without change...')
|
||||
'this text should come out of the template without change...')
|
||||
|
||||
assert_template_result('blah','blah')
|
||||
assert_template_result('<blah>','<blah>')
|
||||
assert_template_result('|,.:','|,.:')
|
||||
assert_template_result('','')
|
||||
assert_template_result('blah', 'blah')
|
||||
assert_template_result('<blah>', '<blah>')
|
||||
assert_template_result('|,.:', '|,.:')
|
||||
assert_template_result('', '')
|
||||
|
||||
text = %|this shouldnt see any transformation either but has multiple lines
|
||||
as you can clearly see here ...|
|
||||
assert_template_result(text,text)
|
||||
text = %(this shouldnt see any transformation either but has multiple lines
|
||||
as you can clearly see here ...)
|
||||
assert_template_result(text, text)
|
||||
end
|
||||
|
||||
def test_has_a_block_which_does_nothing
|
||||
assert_template_result(%|the comment block should be removed .. right?|,
|
||||
%|the comment block should be removed {%comment%} be gone.. {%endcomment%} .. right?|)
|
||||
assert_template_result(%(the comment block should be removed .. right?),
|
||||
%(the comment block should be removed {%comment%} be gone.. {%endcomment%} .. right?))
|
||||
|
||||
assert_template_result('','{%comment%}{%endcomment%}')
|
||||
assert_template_result('','{%comment%}{% endcomment %}')
|
||||
assert_template_result('','{% comment %}{%endcomment%}')
|
||||
assert_template_result('','{% comment %}{% endcomment %}')
|
||||
assert_template_result('','{%comment%}comment{%endcomment%}')
|
||||
assert_template_result('','{% comment %}comment{% endcomment %}')
|
||||
assert_template_result('','{% comment %} 1 {% comment %} 2 {% endcomment %} 3 {% endcomment %}')
|
||||
assert_template_result('', '{%comment%}{%endcomment%}')
|
||||
assert_template_result('', '{%comment%}{% endcomment %}')
|
||||
assert_template_result('', '{% comment %}{%endcomment%}')
|
||||
assert_template_result('', '{% comment %}{% endcomment %}')
|
||||
assert_template_result('', '{%comment%}comment{%endcomment%}')
|
||||
assert_template_result('', '{% comment %}comment{% endcomment %}')
|
||||
assert_template_result('', '{% comment %} 1 {% comment %} 2 {% endcomment %} 3 {% endcomment %}')
|
||||
|
||||
assert_template_result('','{%comment%}{%blabla%}{%endcomment%}')
|
||||
assert_template_result('','{% comment %}{% blabla %}{% endcomment %}')
|
||||
assert_template_result('','{%comment%}{% endif %}{%endcomment%}')
|
||||
assert_template_result('','{% comment %}{% endwhatever %}{% endcomment %}')
|
||||
assert_template_result('','{% comment %}{% raw %} {{%%%%}} }} { {% endcomment %} {% comment {% endraw %} {% endcomment %}')
|
||||
assert_template_result('', '{%comment%}{%blabla%}{%endcomment%}')
|
||||
assert_template_result('', '{% comment %}{% blabla %}{% endcomment %}')
|
||||
assert_template_result('', '{%comment%}{% endif %}{%endcomment%}')
|
||||
assert_template_result('', '{% comment %}{% endwhatever %}{% endcomment %}')
|
||||
assert_template_result('', '{% comment %}{% raw %} {{%%%%}} }} { {% endcomment %} {% comment {% endraw %} {% endcomment %}')
|
||||
|
||||
assert_template_result('foobar','foo{%comment%}comment{%endcomment%}bar')
|
||||
assert_template_result('foobar','foo{% comment %}comment{% endcomment %}bar')
|
||||
assert_template_result('foobar','foo{%comment%} comment {%endcomment%}bar')
|
||||
assert_template_result('foobar','foo{% comment %} comment {% endcomment %}bar')
|
||||
assert_template_result('foobar', 'foo{%comment%}comment{%endcomment%}bar')
|
||||
assert_template_result('foobar', 'foo{% comment %}comment{% endcomment %}bar')
|
||||
assert_template_result('foobar', 'foo{%comment%} comment {%endcomment%}bar')
|
||||
assert_template_result('foobar', 'foo{% comment %} comment {% endcomment %}bar')
|
||||
|
||||
assert_template_result('foo bar','foo {%comment%} {%endcomment%} bar')
|
||||
assert_template_result('foo bar','foo {%comment%}comment{%endcomment%} bar')
|
||||
assert_template_result('foo bar','foo {%comment%} comment {%endcomment%} bar')
|
||||
assert_template_result('foo bar', 'foo {%comment%} {%endcomment%} bar')
|
||||
assert_template_result('foo bar', 'foo {%comment%}comment{%endcomment%} bar')
|
||||
assert_template_result('foo bar', 'foo {%comment%} comment {%endcomment%} bar')
|
||||
|
||||
assert_template_result('foobar','foo{%comment%}
|
||||
assert_template_result('foobar', 'foo{%comment%}
|
||||
{%endcomment%}bar')
|
||||
end
|
||||
|
||||
def test_hyphenated_assign
|
||||
assigns = {'a-b' => '1' }
|
||||
assigns = { 'a-b' => '1' }
|
||||
assert_template_result('a-b:1 a-b:2', 'a-b:{{a-b}} {%assign a-b = 2 %}a-b:{{a-b}}', assigns)
|
||||
end
|
||||
|
||||
def test_assign_with_colon_and_spaces
|
||||
assigns = {'var' => {'a:b c' => {'paged' => '1' }}}
|
||||
assigns = { 'var' => { 'a:b c' => { 'paged' => '1' } } }
|
||||
assert_template_result('var2: 1', '{%assign var2 = var["a:b c"].paged %}var2: {{var2}}', assigns)
|
||||
end
|
||||
|
||||
def test_capture
|
||||
assigns = {'var' => 'content' }
|
||||
assigns = { 'var' => 'content' }
|
||||
assert_template_result('content foo content foo ',
|
||||
'{{ var2 }}{% capture var2 %}{{ var }} foo {% endcapture %}{{ var2 }}{{ var2 }}',
|
||||
assigns)
|
||||
'{{ var2 }}{% capture var2 %}{{ var }} foo {% endcapture %}{{ var2 }}{{ var2 }}',
|
||||
assigns)
|
||||
end
|
||||
|
||||
def test_capture_detects_bad_syntax
|
||||
assert_raise(SyntaxError) do
|
||||
assert_raises(SyntaxError) do
|
||||
assert_template_result('content foo content foo ',
|
||||
'{{ var2 }}{% capture %}{{ var }} foo {% endcapture %}{{ var2 }}{{ var2 }}',
|
||||
{'var' => 'content' })
|
||||
'{{ var2 }}{% capture %}{{ var }} foo {% endcapture %}{{ var2 }}{{ var2 }}',
|
||||
{ 'var' => 'content' })
|
||||
end
|
||||
end
|
||||
|
||||
def test_case
|
||||
assigns = {'condition' => 2 }
|
||||
assigns = { 'condition' => 2 }
|
||||
assert_template_result(' its 2 ',
|
||||
'{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}',
|
||||
assigns)
|
||||
'{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}',
|
||||
assigns)
|
||||
|
||||
assigns = {'condition' => 1 }
|
||||
assigns = { 'condition' => 1 }
|
||||
assert_template_result(' its 1 ',
|
||||
'{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}',
|
||||
assigns)
|
||||
'{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}',
|
||||
assigns)
|
||||
|
||||
assigns = {'condition' => 3 }
|
||||
assigns = { 'condition' => 3 }
|
||||
assert_template_result('',
|
||||
'{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}',
|
||||
assigns)
|
||||
'{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}',
|
||||
assigns)
|
||||
|
||||
assigns = {'condition' => "string here" }
|
||||
assigns = { 'condition' => "string here" }
|
||||
assert_template_result(' hit ',
|
||||
'{% case condition %}{% when "string here" %} hit {% endcase %}',
|
||||
assigns)
|
||||
'{% case condition %}{% when "string here" %} hit {% endcase %}',
|
||||
assigns)
|
||||
|
||||
assigns = {'condition' => "bad string here" }
|
||||
assigns = { 'condition' => "bad string here" }
|
||||
assert_template_result('',
|
||||
'{% case condition %}{% when "string here" %} hit {% endcase %}',\
|
||||
assigns)
|
||||
'{% case condition %}{% when "string here" %} hit {% endcase %}',\
|
||||
assigns)
|
||||
end
|
||||
|
||||
def test_case_with_else
|
||||
assigns = {'condition' => 5 }
|
||||
assigns = { 'condition' => 5 }
|
||||
assert_template_result(' hit ',
|
||||
'{% case condition %}{% when 5 %} hit {% else %} else {% endcase %}',
|
||||
assigns)
|
||||
'{% case condition %}{% when 5 %} hit {% else %} else {% endcase %}',
|
||||
assigns)
|
||||
|
||||
assigns = {'condition' => 6 }
|
||||
assigns = { 'condition' => 6 }
|
||||
assert_template_result(' else ',
|
||||
'{% case condition %}{% when 5 %} hit {% else %} else {% endcase %}',
|
||||
assigns)
|
||||
'{% case condition %}{% when 5 %} hit {% else %} else {% endcase %}',
|
||||
assigns)
|
||||
|
||||
assigns = {'condition' => 6 }
|
||||
assigns = { 'condition' => 6 }
|
||||
assert_template_result(' else ',
|
||||
'{% case condition %} {% when 5 %} hit {% else %} else {% endcase %}',
|
||||
assigns)
|
||||
'{% case condition %} {% when 5 %} hit {% else %} else {% endcase %}',
|
||||
assigns)
|
||||
end
|
||||
|
||||
def test_case_on_size
|
||||
@@ -128,87 +128,87 @@ class StandardTagTest < Test::Unit::TestCase
|
||||
|
||||
def test_case_on_size_with_else
|
||||
assert_template_result('else',
|
||||
'{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}',
|
||||
'a' => [])
|
||||
'{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}',
|
||||
'a' => [])
|
||||
|
||||
assert_template_result('1',
|
||||
'{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}',
|
||||
'a' => [1])
|
||||
'{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}',
|
||||
'a' => [1])
|
||||
|
||||
assert_template_result('2',
|
||||
'{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}',
|
||||
'a' => [1, 1])
|
||||
'{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}',
|
||||
'a' => [1, 1])
|
||||
|
||||
assert_template_result('else',
|
||||
'{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}',
|
||||
'a' => [1, 1, 1])
|
||||
'{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}',
|
||||
'a' => [1, 1, 1])
|
||||
|
||||
assert_template_result('else',
|
||||
'{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}',
|
||||
'a' => [1, 1, 1, 1])
|
||||
'{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}',
|
||||
'a' => [1, 1, 1, 1])
|
||||
|
||||
assert_template_result('else',
|
||||
'{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}',
|
||||
'a' => [1, 1, 1, 1, 1])
|
||||
'{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}',
|
||||
'a' => [1, 1, 1, 1, 1])
|
||||
end
|
||||
|
||||
def test_case_on_length_with_else
|
||||
assert_template_result('else',
|
||||
'{% case a.empty? %}{% when true %}true{% when false %}false{% else %}else{% endcase %}',
|
||||
{})
|
||||
'{% case a.empty? %}{% when true %}true{% when false %}false{% else %}else{% endcase %}',
|
||||
{})
|
||||
|
||||
assert_template_result('false',
|
||||
'{% case false %}{% when true %}true{% when false %}false{% else %}else{% endcase %}',
|
||||
{})
|
||||
'{% case false %}{% when true %}true{% when false %}false{% else %}else{% endcase %}',
|
||||
{})
|
||||
|
||||
assert_template_result('true',
|
||||
'{% case true %}{% when true %}true{% when false %}false{% else %}else{% endcase %}',
|
||||
{})
|
||||
'{% case true %}{% when true %}true{% when false %}false{% else %}else{% endcase %}',
|
||||
{})
|
||||
|
||||
assert_template_result('else',
|
||||
'{% case NULL %}{% when true %}true{% when false %}false{% else %}else{% endcase %}',
|
||||
{})
|
||||
'{% case NULL %}{% when true %}true{% when false %}false{% else %}else{% endcase %}',
|
||||
{})
|
||||
end
|
||||
|
||||
def test_assign_from_case
|
||||
# Example from the shopify forums
|
||||
code = %q({% case collection.handle %}{% when 'menswear-jackets' %}{% assign ptitle = 'menswear' %}{% when 'menswear-t-shirts' %}{% assign ptitle = 'menswear' %}{% else %}{% assign ptitle = 'womenswear' %}{% endcase %}{{ ptitle }})
|
||||
code = "{% case collection.handle %}{% when 'menswear-jackets' %}{% assign ptitle = 'menswear' %}{% when 'menswear-t-shirts' %}{% assign ptitle = 'menswear' %}{% else %}{% assign ptitle = 'womenswear' %}{% endcase %}{{ ptitle }}"
|
||||
template = Liquid::Template.parse(code)
|
||||
assert_equal "menswear", template.render!("collection" => {'handle' => 'menswear-jackets'})
|
||||
assert_equal "menswear", template.render!("collection" => {'handle' => 'menswear-t-shirts'})
|
||||
assert_equal "womenswear", template.render!("collection" => {'handle' => 'x'})
|
||||
assert_equal "womenswear", template.render!("collection" => {'handle' => 'y'})
|
||||
assert_equal "womenswear", template.render!("collection" => {'handle' => 'z'})
|
||||
assert_equal "menswear", template.render!("collection" => { 'handle' => 'menswear-jackets' })
|
||||
assert_equal "menswear", template.render!("collection" => { 'handle' => 'menswear-t-shirts' })
|
||||
assert_equal "womenswear", template.render!("collection" => { 'handle' => 'x' })
|
||||
assert_equal "womenswear", template.render!("collection" => { 'handle' => 'y' })
|
||||
assert_equal "womenswear", template.render!("collection" => { 'handle' => 'z' })
|
||||
end
|
||||
|
||||
def test_case_when_or
|
||||
code = '{% case condition %}{% when 1 or 2 or 3 %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}'
|
||||
assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 1 })
|
||||
assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 2 })
|
||||
assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 3 })
|
||||
assert_template_result(' its 4 ', code, {'condition' => 4 })
|
||||
assert_template_result('', code, {'condition' => 5 })
|
||||
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 1 })
|
||||
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 2 })
|
||||
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 3 })
|
||||
assert_template_result(' its 4 ', code, { 'condition' => 4 })
|
||||
assert_template_result('', code, { 'condition' => 5 })
|
||||
|
||||
code = '{% case condition %}{% when 1 or "string" or null %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}'
|
||||
assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 1 })
|
||||
assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 'string' })
|
||||
assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => nil })
|
||||
assert_template_result('', code, {'condition' => 'something else' })
|
||||
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 1 })
|
||||
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 'string' })
|
||||
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => nil })
|
||||
assert_template_result('', code, { 'condition' => 'something else' })
|
||||
end
|
||||
|
||||
def test_case_when_comma
|
||||
code = '{% case condition %}{% when 1, 2, 3 %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}'
|
||||
assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 1 })
|
||||
assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 2 })
|
||||
assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 3 })
|
||||
assert_template_result(' its 4 ', code, {'condition' => 4 })
|
||||
assert_template_result('', code, {'condition' => 5 })
|
||||
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 1 })
|
||||
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 2 })
|
||||
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 3 })
|
||||
assert_template_result(' its 4 ', code, { 'condition' => 4 })
|
||||
assert_template_result('', code, { 'condition' => 5 })
|
||||
|
||||
code = '{% case condition %}{% when 1, "string", null %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}'
|
||||
assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 1 })
|
||||
assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 'string' })
|
||||
assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => nil })
|
||||
assert_template_result('', code, {'condition' => 'something else' })
|
||||
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 1 })
|
||||
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 'string' })
|
||||
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => nil })
|
||||
assert_template_result('', code, { 'condition' => 'something else' })
|
||||
end
|
||||
|
||||
def test_assign
|
||||
@@ -229,22 +229,21 @@ class StandardTagTest < Test::Unit::TestCase
|
||||
end
|
||||
|
||||
def test_case_detects_bad_syntax
|
||||
assert_raise(SyntaxError) do
|
||||
assert_raises(SyntaxError) do
|
||||
assert_template_result('', '{% case false %}{% when %}true{% endcase %}', {})
|
||||
end
|
||||
|
||||
assert_raise(SyntaxError) do
|
||||
assert_raises(SyntaxError) do
|
||||
assert_template_result('', '{% case false %}{% huh %}true{% endcase %}', {})
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def test_cycle
|
||||
assert_template_result('one','{%cycle "one", "two"%}')
|
||||
assert_template_result('one two','{%cycle "one", "two"%} {%cycle "one", "two"%}')
|
||||
assert_template_result(' two','{%cycle "", "two"%} {%cycle "", "two"%}')
|
||||
assert_template_result('one', '{%cycle "one", "two"%}')
|
||||
assert_template_result('one two', '{%cycle "one", "two"%} {%cycle "one", "two"%}')
|
||||
assert_template_result(' two', '{%cycle "", "two"%} {%cycle "", "two"%}')
|
||||
|
||||
assert_template_result('one two one','{%cycle "one", "two"%} {%cycle "one", "two"%} {%cycle "one", "two"%}')
|
||||
assert_template_result('one two one', '{%cycle "one", "two"%} {%cycle "one", "two"%} {%cycle "one", "two"%}')
|
||||
|
||||
assert_template_result('text-align: left text-align: right',
|
||||
'{%cycle "text-align: left", "text-align: right" %} {%cycle "text-align: left", "text-align: right"%}')
|
||||
@@ -261,18 +260,18 @@ class StandardTagTest < Test::Unit::TestCase
|
||||
end
|
||||
|
||||
def test_multiple_named_cycles_with_names_from_context
|
||||
assigns = {"var1" => 1, "var2" => 2 }
|
||||
assigns = { "var1" => 1, "var2" => 2 }
|
||||
assert_template_result('one one two two one one',
|
||||
'{%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %} {%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %} {%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %}', assigns)
|
||||
end
|
||||
|
||||
def test_size_of_array
|
||||
assigns = {"array" => [1,2,3,4]}
|
||||
assigns = { "array" => [1, 2, 3, 4] }
|
||||
assert_template_result('array has 4 elements', "array has {{ array.size }} elements", assigns)
|
||||
end
|
||||
|
||||
def test_size_of_hash
|
||||
assigns = {"hash" => {:a => 1, :b => 2, :c=> 3, :d => 4}}
|
||||
assigns = { "hash" => { a: 1, b: 2, c: 3, d: 4 } }
|
||||
assert_template_result('hash has 4 elements', "hash has {{ hash.size }} elements", assigns)
|
||||
end
|
||||
|
||||
@@ -284,11 +283,11 @@ class StandardTagTest < Test::Unit::TestCase
|
||||
end
|
||||
|
||||
def test_ifchanged
|
||||
assigns = {'array' => [ 1, 1, 2, 2, 3, 3] }
|
||||
assert_template_result('123','{%for item in array%}{%ifchanged%}{{item}}{% endifchanged %}{%endfor%}',assigns)
|
||||
assigns = { 'array' => [ 1, 1, 2, 2, 3, 3] }
|
||||
assert_template_result('123', '{%for item in array%}{%ifchanged%}{{item}}{% endifchanged %}{%endfor%}', assigns)
|
||||
|
||||
assigns = {'array' => [ 1, 1, 1, 1] }
|
||||
assert_template_result('1','{%for item in array%}{%ifchanged%}{{item}}{% endifchanged %}{%endfor%}',assigns)
|
||||
assigns = { 'array' => [ 1, 1, 1, 1] }
|
||||
assert_template_result('1', '{%for item in array%}{%ifchanged%}{{item}}{% endifchanged %}{%endfor%}', assigns)
|
||||
end
|
||||
|
||||
def test_multiline_tag
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
require 'test_helper'
|
||||
|
||||
class StatementsTest < Test::Unit::TestCase
|
||||
class StatementsTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_true_eql_true
|
||||
@@ -37,7 +37,6 @@ class StatementsTest < Test::Unit::TestCase
|
||||
text = ' {% if null <= 0 %} true {% else %} false {% endif %} '
|
||||
assert_template_result ' false ', text
|
||||
|
||||
|
||||
text = ' {% if 0 <= null %} true {% else %} false {% endif %} '
|
||||
assert_template_result ' false ', text
|
||||
end
|
||||
@@ -72,18 +71,17 @@ class StatementsTest < Test::Unit::TestCase
|
||||
assert_template_result ' true ', text, 'var' => 'hello there!'
|
||||
end
|
||||
|
||||
|
||||
def test_var_and_long_string_are_equal_backwards
|
||||
text = " {% if 'hello there!' == var %} true {% else %} false {% endif %} "
|
||||
assert_template_result ' true ', text, 'var' => 'hello there!'
|
||||
end
|
||||
|
||||
#def test_is_nil
|
||||
# def test_is_nil
|
||||
# text = %| {% if var != nil %} true {% else %} false {% end %} |
|
||||
# @template.assigns = { 'var' => 'hello there!'}
|
||||
# expected = %| true |
|
||||
# assert_equal expected, @template.parse(text)
|
||||
#end
|
||||
# end
|
||||
|
||||
def test_is_collection_empty
|
||||
text = ' {% if array == empty %} true {% else %} false {% endif %} '
|
||||
@@ -92,7 +90,7 @@ class StatementsTest < Test::Unit::TestCase
|
||||
|
||||
def test_is_not_collection_empty
|
||||
text = ' {% if array == empty %} true {% else %} false {% endif %} '
|
||||
assert_template_result ' false ', text, 'array' => [1,2,3]
|
||||
assert_template_result ' false ', text, 'array' => [1, 2, 3]
|
||||
end
|
||||
|
||||
def test_nil
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
require 'test_helper'
|
||||
|
||||
class TableRowTest < Test::Unit::TestCase
|
||||
class TableRowTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
class ArrayDrop < Liquid::Drop
|
||||
@@ -16,48 +16,49 @@ class TableRowTest < Test::Unit::TestCase
|
||||
end
|
||||
|
||||
def test_table_row
|
||||
|
||||
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
|
||||
'{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}',
|
||||
'numbers' => [1,2,3,4,5,6])
|
||||
'{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}',
|
||||
'numbers' => [1, 2, 3, 4, 5, 6])
|
||||
|
||||
assert_template_result("<tr class=\"row1\">\n</tr>\n",
|
||||
'{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}',
|
||||
'numbers' => [])
|
||||
'{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}',
|
||||
'numbers' => [])
|
||||
end
|
||||
|
||||
def test_table_row_with_different_cols
|
||||
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td><td class=\"col4\"> 4 </td><td class=\"col5\"> 5 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 6 </td></tr>\n",
|
||||
'{% tablerow n in numbers cols:5%} {{n}} {% endtablerow %}',
|
||||
'numbers' => [1,2,3,4,5,6])
|
||||
|
||||
'{% tablerow n in numbers cols:5%} {{n}} {% endtablerow %}',
|
||||
'numbers' => [1, 2, 3, 4, 5, 6])
|
||||
end
|
||||
|
||||
def test_table_col_counter
|
||||
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\">1</td><td class=\"col2\">2</td></tr>\n<tr class=\"row2\"><td class=\"col1\">1</td><td class=\"col2\">2</td></tr>\n<tr class=\"row3\"><td class=\"col1\">1</td><td class=\"col2\">2</td></tr>\n",
|
||||
'{% tablerow n in numbers cols:2%}{{tablerowloop.col}}{% endtablerow %}',
|
||||
'numbers' => [1,2,3,4,5,6])
|
||||
'{% tablerow n in numbers cols:2%}{{tablerowloop.col}}{% endtablerow %}',
|
||||
'numbers' => [1, 2, 3, 4, 5, 6])
|
||||
end
|
||||
|
||||
def test_quoted_fragment
|
||||
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
|
||||
"{% tablerow n in collections.frontpage cols:3%} {{n}} {% endtablerow %}",
|
||||
'collections' => {'frontpage' => [1,2,3,4,5,6]})
|
||||
"{% tablerow n in collections.frontpage cols:3%} {{n}} {% endtablerow %}",
|
||||
'collections' => { 'frontpage' => [1, 2, 3, 4, 5, 6] })
|
||||
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
|
||||
"{% tablerow n in collections['frontpage'] cols:3%} {{n}} {% endtablerow %}",
|
||||
'collections' => {'frontpage' => [1,2,3,4,5,6]})
|
||||
|
||||
"{% tablerow n in collections['frontpage'] cols:3%} {{n}} {% endtablerow %}",
|
||||
'collections' => { 'frontpage' => [1, 2, 3, 4, 5, 6] })
|
||||
end
|
||||
|
||||
def test_enumerable_drop
|
||||
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
|
||||
'{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}',
|
||||
'numbers' => ArrayDrop.new([1,2,3,4,5,6]))
|
||||
'{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}',
|
||||
'numbers' => ArrayDrop.new([1, 2, 3, 4, 5, 6]))
|
||||
end
|
||||
|
||||
def test_offset_and_limit
|
||||
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
|
||||
'{% tablerow n in numbers cols:3 offset:1 limit:6%} {{n}} {% endtablerow %}',
|
||||
'numbers' => [0,1,2,3,4,5,6,7])
|
||||
'{% tablerow n in numbers cols:3 offset:1 limit:6%} {{n}} {% endtablerow %}',
|
||||
'numbers' => [0, 1, 2, 3, 4, 5, 6, 7])
|
||||
end
|
||||
|
||||
def test_blank_string_not_iterable
|
||||
assert_template_result("<tr class=\"row1\">\n</tr>\n", "{% tablerow char in characters cols:3 %}I WILL NOT BE OUTPUT{% endtablerow %}", 'characters' => '')
|
||||
end
|
||||
end
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user