mirror of
https://github.com/Shopify/liquid.git
synced 2025-08-18 00:00:32 -04:00
Compare commits
921 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
200fa0c11b | ||
|
27ead517ee | ||
|
0b9318222b | ||
|
21d6197533 | ||
|
5e92b3a89a | ||
|
7f2cf1fe67 | ||
|
10e0fb795e | ||
|
546dd9bc06 | ||
|
9a77e3e923 | ||
|
c44d1d9193 | ||
|
dd7bbf26bc | ||
|
cca24a2226 | ||
|
98ce25cb40 | ||
|
77293d4524 | ||
|
af66bc8a5f | ||
|
42e5c52336 | ||
|
649cca1349 | ||
|
81ed65f2a1 | ||
|
6ca06c22b8 | ||
|
80bc7ffdf2 | ||
|
48cb643c02 | ||
|
1d97389fb0 | ||
|
3ff4170cb0 | ||
|
24dceef552 | ||
|
428c66ffac | ||
|
0fe4a5d144 | ||
|
e650dc4195 | ||
|
a75517e2c7 | ||
|
9ab688eada | ||
|
abef59d129 | ||
|
940c3a4207 | ||
|
84a0289ebc | ||
|
4599e5459f | ||
|
59c445f0e1 | ||
|
bd9c3802c8 | ||
|
2b40850e4a | ||
|
22ded5f304 | ||
|
ddc32b7bd8 | ||
|
74e505f6fa | ||
|
6a888d4564 | ||
|
dd257b3d66 | ||
|
1aaf6ed019 | ||
|
daf93a83c2 | ||
|
e889a9da0b | ||
|
0f11c97623 | ||
|
e804f36681 | ||
|
619ed3fcd7 | ||
|
cdb5cb06b2 | ||
|
bc153159e6 | ||
|
128b4e35be | ||
|
c743936a78 | ||
|
bf711a0521 | ||
|
e8731f27d9 | ||
|
6a44c1ec77 | ||
|
1beb87b446 | ||
|
b839deb3a8 | ||
|
0b826120c0 | ||
|
936f803a4e | ||
|
5cd8a83fa6 | ||
|
c2c6cb2b15 | ||
|
9ccbf64571 | ||
|
8b1b9f649a | ||
|
474315c6bb | ||
|
667664bb22 | ||
|
1cdd1f0834 | ||
|
1cd5ec54f0 | ||
|
29732f4305 | ||
|
abed47547c | ||
|
ace3fe15ac | ||
|
db8e85ab31 | ||
|
f484b868d0 | ||
|
3e8994c258 | ||
|
7ccac29688 | ||
|
96b5325f87 | ||
|
b8f05267a9 | ||
|
6ce4ec1011 | ||
|
a39422feac | ||
|
2c2f5826d5 | ||
|
c99c93255d | ||
|
c0c191cabd | ||
|
6765d93938 | ||
|
4f17abfb4a | ||
|
eff2a63204 | ||
|
fbab19ac8c | ||
|
b14bf94c98 | ||
|
951be6c1f5 | ||
|
c8f3cfa8fc | ||
|
f21d1c5d9d | ||
|
456be2f75e | ||
|
ff1c35b986 | ||
|
cab08cfe57 | ||
|
bb7027138e | ||
|
34512df8e9 | ||
|
bca01e8944 | ||
|
d3647e280d | ||
|
c0f565ce7b | ||
|
753015d8fc | ||
|
f4e32d2214 | ||
|
8adbbfeaa6 | ||
|
4648f0fa64 | ||
|
3a64b3741f | ||
|
2c51a1922a | ||
|
57fdc1b5fb | ||
|
308dfc3cb6 | ||
|
2515f3be09 | ||
|
433ed0fff2 | ||
|
0787660603 | ||
|
93c252fe5a | ||
|
df13389940 | ||
|
ca2d850eea | ||
|
3a736da222 | ||
|
eb89f22d93 | ||
|
98e146ebf7 | ||
|
7611463f02 | ||
|
f1846d63a3 | ||
|
af3f8612bf | ||
|
6f8722a6d3 | ||
|
81f44e36be | ||
|
c9ec8f4635 | ||
|
3fb467f069 | ||
|
992e15a173 | ||
|
0bb6539dce | ||
|
1fdc577246 | ||
|
74245cd396 | ||
|
86605016e1 | ||
|
6981305736 | ||
|
6d3c5ef3d3 | ||
|
150ddf4c3b | ||
|
a3e9088a0e | ||
|
b05393884d | ||
|
ef9db08642 | ||
|
5dd8c84b47 | ||
|
1af12c74cc | ||
|
72bbbda022 | ||
|
489e3ca7bf | ||
|
65542f9e4f | ||
|
d497bfffe9 | ||
|
ebafb0a3fe | ||
|
4ec9db3f99 | ||
|
95eb5d6036 | ||
|
7ba40d48c0 | ||
|
9020dbcd41 | ||
|
73f7467258 | ||
|
f3aa5fbd7c | ||
|
eb70bb9b87 | ||
|
4d8e55dbc4 | ||
|
f697093e94 | ||
|
7594bed88a | ||
|
f64471eb4e | ||
|
d4c24f3ce2 | ||
|
3bbb7aa7ba | ||
|
102bac2e33 | ||
|
e69f729f76 | ||
|
4ec0b85d80 | ||
|
f77c766262 | ||
|
7bc14ae2be | ||
|
41c19929ad | ||
|
05d768c6ab | ||
|
54414dfd83 | ||
|
23a8438fa6 | ||
|
21f3337dec | ||
|
1f0a0ad55c | ||
|
36dce29776 | ||
|
8b68630a11 | ||
|
8882338aa1 | ||
|
6c2c621712 | ||
|
1cae1e497f | ||
|
ed7dae50aa | ||
|
7e99432bd1 | ||
|
6c187b8470 | ||
|
6e07f73f68 | ||
|
f64af57b7b | ||
|
c60c3c7802 | ||
|
11625b1bc9 | ||
|
ec6fb4d5fa | ||
|
fad58ef436 | ||
|
22568080b1 | ||
|
dd7ed00ec4 | ||
|
1667c1180e | ||
|
7357dcf185 | ||
|
68c3827ef2 | ||
|
4af38bc549 | ||
|
df241abf70 | ||
|
10f8337209 | ||
|
5ed0410a8b | ||
|
0f5220c391 | ||
|
7a23f46fab | ||
|
3f7edf00b9 | ||
|
b4a2a79e26 | ||
|
1d2bee1f60 | ||
|
01e6eec97a | ||
|
fbdab19358 | ||
|
ce85ac5d3d | ||
|
c0ffee16a3 | ||
|
a7eb33fa39 | ||
|
1a85e98793 | ||
|
c588337aac | ||
|
97f7922457 | ||
|
0d83e64cfe | ||
|
0d5e01ae98 | ||
|
15eaa49e48 | ||
|
91c54c579d | ||
|
1310c4978d | ||
|
3de1db3c3a | ||
|
03522caaf8 | ||
|
7acea2a9c9 | ||
|
d8ef698539 | ||
|
ebdfdb80e5 | ||
|
95e9fa5010 | ||
|
0e14d539a3 | ||
|
db106ae058 | ||
|
db3999a008 | ||
|
10e2aa8d5b | ||
|
7c4114671b | ||
|
b01de9d325 | ||
|
a5369c26a8 | ||
|
a03de8f9ea | ||
|
e86fe27259 | ||
|
c8906d05b9 | ||
|
50d1a2ffc9 | ||
|
f686c5dec7 | ||
|
aa8ce87b96 | ||
|
698f5e0d96 | ||
|
996bfe0c82 | ||
|
be81c9ae5a | ||
|
edd4d70aee | ||
|
ac66dbbafe | ||
|
017c1b5e83 | ||
|
250555c9a8 | ||
|
e361a4d53c | ||
|
b9e0d28729 | ||
|
020f6b93c5 | ||
|
cfe1637bdd | ||
|
eab13a07d9 | ||
|
ca96ca0fef | ||
|
4e7a953e73 | ||
|
6ac2499f7f | ||
|
ff70161512 | ||
|
026157e128 | ||
|
bf64239ea6 | ||
|
c270a6f378 | ||
|
4fba61a802 | ||
|
6b6baece25 | ||
|
15b2d193ec | ||
|
fd712d134a | ||
|
0c2db998cf | ||
|
9dac68cce1 | ||
|
c50509b741 | ||
|
cd66572514 | ||
|
dcb5a67089 | ||
|
efe44a7e6a | ||
|
8625e66453 | ||
|
3cae09b968 | ||
|
3c499d0241 | ||
|
e71e53ffb5 | ||
|
260c863e23 | ||
|
42b6c07cd0 | ||
|
c91a6827f2 | ||
|
5dbc3d5701 | ||
|
22683cbd2a | ||
|
abfab3bef2 | ||
|
51e8d6234a | ||
|
7ca2846d9c | ||
|
7ba0fc7952 | ||
|
e2c86d137f | ||
|
776a63b61d | ||
|
84f9d6957c | ||
|
7d32728e16 | ||
|
40a9b72b3c | ||
|
4ff26cd707 | ||
|
462919a28f | ||
|
f3e2be9f85 | ||
|
4d40f83457 | ||
|
00be1e4dd4 | ||
|
f7d67b946e | ||
|
ae9aee896b | ||
|
6dec172743 | ||
|
da581d988a | ||
|
7960826552 | ||
|
84059691b8 | ||
|
896288eff1 | ||
|
b3f132efd1 | ||
|
60214b957c | ||
|
7361220af6 | ||
|
cb2ad71a31 | ||
|
900e3a6491 | ||
|
f18084203d | ||
|
3358a892f2 | ||
|
bbfcaa2cc0 | ||
|
ba657871bc | ||
|
29d5d9674a | ||
|
0a645e72c1 | ||
|
1850511334 | ||
|
300adfd7ae | ||
|
f357662f37 | ||
|
ed0aebcbc9 | ||
|
ea4f1885f8 | ||
|
2f75db604f | ||
|
d844a3dd8b | ||
|
9fcba1a26c | ||
|
0659891e68 | ||
|
e7fb3b18f3 | ||
|
e6eef4b2c4 | ||
|
2ce577e36b | ||
|
c7c21e88f0 | ||
|
a89371b0b9 | ||
|
8f7f8761d1 | ||
|
a3ff300419 | ||
|
ea6e326b9c | ||
|
740f8759cc | ||
|
bb9cd4eb6a | ||
|
3a591fbf26 | ||
|
7754d5aef5 | ||
|
1d63d5db5f | ||
|
26640368e5 | ||
|
f23c2a83f2 | ||
|
61d54d1b19 | ||
|
10ea6144e0 | ||
|
292d971937 | ||
|
5c082472a1 | ||
|
0bedc71854 | ||
|
fe66edb825 | ||
|
bfa2df7036 | ||
|
0e52706a5b | ||
|
4c6166f989 | ||
|
8e99b3bd7f | ||
|
f6532de1fd | ||
|
001fde7694 | ||
|
b872eac2b9 | ||
|
038d0585cf | ||
|
b15428ea83 | ||
|
c9ad9d338c | ||
|
ae6bd9f6b0 | ||
|
866e437c05 | ||
|
784db053f2 | ||
|
ff1c6bd26e | ||
|
46fd63da5f | ||
|
420a1c79e1 | ||
|
6d39050e1e | ||
|
077bf2a409 | ||
|
1a3e38c018 | ||
|
e495f75cc2 | ||
|
e781449c36 | ||
|
7eb03ea198 | ||
|
bd34cd5613 | ||
|
c28d455f7b | ||
|
d250a7f502 | ||
|
b0f46326ca | ||
|
7aed2f122c | ||
|
5199a34d9b | ||
|
4c2ab6f878 | ||
|
a818dd9d19 | ||
|
efef03d944 | ||
|
33760f083a | ||
|
013802c877 | ||
|
3dcad3b3cd | ||
|
db065315ba | ||
|
a03f02789b | ||
|
ca4b9b43af | ||
|
77084930e9 | ||
|
fb77921b15 | ||
|
0d02dea20b | ||
|
86b47ba28b | ||
|
95ff0595c6 | ||
|
bbc56f35ec | ||
|
dfbbf87ba9 | ||
|
037b603603 | ||
|
bd33df09de | ||
|
6ca5b62112 | ||
|
e1a2057a1b | ||
|
ae9dbe0ca7 | ||
|
3b486425b0 | ||
|
b08bcf00ac | ||
|
0740e8b431 | ||
|
5532df880f | ||
|
2b11efc3ae | ||
|
a1d982ca76 | ||
|
03be7f1ee3 | ||
|
1ced4eaf10 | ||
|
4970167726 | ||
|
065ccbc4aa | ||
|
1feaa63813 | ||
|
8541c6be35 | ||
|
18654526c8 | ||
|
bd1f7f9492 | ||
|
40d75dd283 | ||
|
f5011365f1 | ||
|
ebbd046c92 | ||
|
b9979088ec | ||
|
bd0e53bd2e | ||
|
4b586f4105 | ||
|
0410119d5f | ||
|
c2f67398d0 | ||
|
81149344a5 | ||
|
e9b649b345 | ||
|
9c538f4237 | ||
|
c08a358a2b | ||
|
dbaef5e79b | ||
|
48a155a213 | ||
|
c69a9a77c6 | ||
|
ef79fa3898 | ||
|
f7ad602bfc | ||
|
ffd6049ba2 | ||
|
b3ad54c0c2 | ||
|
67eca3f58d | ||
|
0847bf560f | ||
|
8074565c3e | ||
|
24e81267b9 | ||
|
c0ffee3ff9 | ||
|
c0ffeeef26 | ||
|
22dbf90b7d | ||
|
40c68c9c83 | ||
|
b7f0f158ab | ||
|
d8f31046a9 | ||
|
6c6382ed69 | ||
|
53ba1372f9 | ||
|
57c9cf64eb | ||
|
e83b1e4159 | ||
|
3784020a8d | ||
|
1223444738 | ||
|
2bfeed2b00 | ||
|
04b800d768 | ||
|
f1d62978ef | ||
|
ffadc64f28 | ||
|
5302f40342 | ||
|
b0f8c2c03e | ||
|
37e40673ff | ||
|
fefee4c675 | ||
|
1aa7d3d2ba | ||
|
0db9c56f34 | ||
|
f4d134cd5c | ||
|
b667bcb48b | ||
|
2c14e0b2ba | ||
|
ca207ed93f | ||
|
ef13343591 | ||
|
adb40c41b7 | ||
|
d8403af515 | ||
|
0d26f05bb8 | ||
|
1dcad34b06 | ||
|
9a42c8c8b2 | ||
|
1fcef2133f | ||
|
d7514b1305 | ||
|
c0ffee5919 | ||
|
724d02e9b3 | ||
|
a5b387cdd4 | ||
|
8318be2edc | ||
|
b6547f322e | ||
|
b316ff8413 | ||
|
806b2622da | ||
|
1f90a37b63 | ||
|
c34f7c9b2c | ||
|
604d899496 | ||
|
799da202df | ||
|
ddb45cd658 | ||
|
dafbb4ae90 | ||
|
9876096cf4 | ||
|
8750b4b006 | ||
|
34083c96d5 | ||
|
9672ed5285 | ||
|
f3112fc038 | ||
|
d338ccb9a6 | ||
|
d67de1c9b2 | ||
|
2324564743 | ||
|
b3097f143c | ||
|
7b309dc75d | ||
|
8f68cffdf1 | ||
|
dd27d0fd1d | ||
|
7a26e6b3d8 | ||
|
cf4e77ab0c | ||
|
7bae55dd39 | ||
|
0ce8aef229 | ||
|
6eab595fae | ||
|
b16b109a80 | ||
|
831355dfbd | ||
|
00702d8e63 | ||
|
197c058208 | ||
|
98dfe198e1 | ||
|
c2c1497ca8 | ||
|
d19967a79d | ||
|
248c54a386 | ||
|
2c42447659 | ||
|
ab698191b9 | ||
|
9ef6f9b642 | ||
|
4684478e94 | ||
|
b3b63a683f | ||
|
1c577c5b62 | ||
|
755d2821f3 | ||
|
495b3d312f | ||
|
9640e77805 | ||
|
453f6348c2 | ||
|
70ed1fc86d | ||
|
2a1ca3152d | ||
|
c2ef247be5 | ||
|
1518d3f6f9 | ||
|
c67b77709d | ||
|
c89ce9c2ed | ||
|
7dc488a73b | ||
|
e6ed804ca5 | ||
|
951abb67ee | ||
|
8d1cd41453 | ||
|
b0629f17f7 | ||
|
274f078806 | ||
|
d7171aa084 | ||
|
06c4789dc5 | ||
|
f2f467bdbc | ||
|
ff99d92c18 | ||
|
39fecd06db | ||
|
8013df8ca2 | ||
|
14cd011cb5 | ||
|
e2d9907df2 | ||
|
23d669f5e6 | ||
|
ed73794f82 | ||
|
f59f6dea83 | ||
|
7a81fb821a | ||
|
cec27ea326 | ||
|
14999e8f7c | ||
|
b41fc10d8e | ||
|
2b3c81cfd0 | ||
|
2a2376bfd9 | ||
|
ca9e75db53 | ||
|
407c8abf30 | ||
|
43f181e211 | ||
|
7c613e87cb | ||
|
fe4034ccf9 | ||
|
52ee303a36 | ||
|
8217a8d86c | ||
|
7d13d88258 | ||
|
ff727016ef | ||
|
c11fc656cf | ||
|
d789ec4175 | ||
|
fd09f049b0 | ||
|
842986a972 | ||
|
4661700a97 | ||
|
cd5a6dd225 | ||
|
89c1ba2b0e | ||
|
479d8fb4a4 | ||
|
53b8babf52 | ||
|
76b4920d3e | ||
|
8dcc319128 | ||
|
0b36461d80 | ||
|
70e75719de | ||
|
b037b19688 | ||
|
d0f77f6cf4 | ||
|
0be260bc97 | ||
|
5f0b64cebc | ||
|
c086017bc9 | ||
|
4369fe6c85 | ||
|
c118e6b435 | ||
|
0fbaf873d9 | ||
|
5980ddbfae | ||
|
193fc0fb7a | ||
|
e4da4d49d2 | ||
|
a0bec1f873 | ||
|
4aa3261518 | ||
|
04d552fabb | ||
|
5106466a2d | ||
|
5d6c1ed7c6 | ||
|
a594653a0c | ||
|
0c802aba17 | ||
|
147d7ae24d | ||
|
282d42f98d | ||
|
e6ba6ee87b | ||
|
2ad7a37d44 | ||
|
4bdaaf069f | ||
|
85b1e91aed | ||
|
a7c5e247c8 | ||
|
6c117fd7dd | ||
|
7d2d90d715 | ||
|
f761d21215 | ||
|
a796c17f8b | ||
|
deb10ebc7a | ||
|
cfe1844de9 | ||
|
59950bff87 | ||
|
27c91203ab | ||
|
44eaa4b9d8 | ||
|
a979b3ec95 | ||
|
bf3e759da3 | ||
|
59162f7a0e | ||
|
c582b86f16 | ||
|
e340803d12 | ||
|
48a6d86ac2 | ||
|
3bb29d5456 | ||
|
9c72ccb82f | ||
|
62d4625468 | ||
|
8928454e29 | ||
|
1370a102c9 | ||
|
c9bac9befe | ||
|
210a0616f3 | ||
|
5149cde5c3 | ||
|
22f2cec5de | ||
|
4318240ae0 | ||
|
aa79c33dda | ||
|
b1ef28566e | ||
|
41bcc48222 | ||
|
27d5106dc9 | ||
|
7334073be2 | ||
|
5dcefd7d77 | ||
|
25c7b05916 | ||
|
d17f86ba4d | ||
|
384e4313ff | ||
|
23f2af8ff5 | ||
|
a93eac0268 | ||
|
2cc7493cb0 | ||
|
85463e1753 | ||
|
52ff9b0e84 | ||
|
0c58328a40 | ||
|
2bb3552033 | ||
|
8b751ddf46 | ||
|
e5cbdb2b27 | ||
|
ffb0ace303 | ||
|
ad00998ef8 | ||
|
869dbc7ebf | ||
|
fae3a2de7b | ||
|
f27bd619b9 | ||
|
a9b84b7806 | ||
|
6cc2c567c5 | ||
|
812e3c51b9 | ||
|
9dd0801f5c | ||
|
b146b49f46 | ||
|
86944fe7b7 | ||
|
a549d289d7 | ||
|
b2feeacbce | ||
|
143ba39a08 | ||
|
43e59796f6 | ||
|
bb3624b799 | ||
|
64fca66ef5 | ||
|
e9d7486758 | ||
|
2bb98c1431 | ||
|
95d5c24bfc | ||
|
b7ee1a2176 | ||
|
0eca61a977 | ||
|
9bfd04da2d | ||
|
302185a7fc | ||
|
6ed6e7e12f | ||
|
f41ed78378 | ||
|
50c85afc35 | ||
|
5876dff326 | ||
|
f25185631d | ||
|
283f1bad18 | ||
|
e1d40c7d89 | ||
|
19c6eb426a | ||
|
f87b06095d | ||
|
b81d54e789 | ||
|
00f53b16e8 | ||
|
e4cf55b112 | ||
|
5bb211d933 | ||
|
6adc431a19 | ||
|
23d2beed41 | ||
|
a80ecb7678 | ||
|
361c695264 | ||
|
f93243cc1a | ||
|
1e533a52e7 | ||
|
3ea84f095f | ||
|
4239c899a4 | ||
|
1597f8859f | ||
|
b3dda384c9 | ||
|
6828670bfe | ||
|
d2f16d92d6 | ||
|
d233acb483 | ||
|
8920e2a2a2 | ||
|
bfee507005 | ||
|
929c89789f | ||
|
d03c4ae8e8 | ||
|
021bafd260 | ||
|
04c393ab07 | ||
|
9a7778e52c | ||
|
dde00253f9 | ||
|
18d1644980 | ||
|
c424d47274 | ||
|
8e6b9d503d | ||
|
8be38d1795 | ||
|
3146d5c3f2 | ||
|
0cc8b68a97 | ||
|
5a50c12953 | ||
|
a6fa4c5c38 | ||
|
dadd9b4dd2 | ||
|
6434b8d2bb | ||
|
2d891ddd8f | ||
|
60b508b151 | ||
|
3891f14a1a | ||
|
198f0aa366 | ||
|
f2e6adf566 | ||
|
08de6ed2c5 | ||
|
7e322f5cf8 | ||
|
bf86a5a069 | ||
|
0141444814 | ||
|
6d30226768 | ||
|
63e8bac1a4 | ||
|
8449849ed5 | ||
|
4bc198a0db | ||
|
3921dbe919 | ||
|
79e2d1d8b4 | ||
|
b7c4041db8 | ||
|
e113c891ec | ||
|
a32ad449c0 | ||
|
1662ba6679 | ||
|
99b5e86f0a | ||
|
b892a73463 | ||
|
0b55d09cea | ||
|
5f8086572b | ||
|
bdb9a4a47f | ||
|
c38eec0293 | ||
|
8d5a907dc8 | ||
|
74cc41ce74 | ||
|
a120cc587a | ||
|
c582023321 | ||
|
ac041c4ad1 | ||
|
31d7682f4e | ||
|
5f1acbc086 | ||
|
8612716129 | ||
|
e6392d1cc1 | ||
|
04381418d3 | ||
|
89ccdabe9a | ||
|
c0fc6777b0 | ||
|
cd03346239 | ||
|
b4f19da127 | ||
|
4100f8d641 | ||
|
d8bda2c892 | ||
|
4f81c0a658 | ||
|
704937bc00 | ||
|
27c6b8074a | ||
|
affae5ebef | ||
|
fc1c0d0d83 | ||
|
a215b70de9 | ||
|
1f70928f8a | ||
|
7713f6709d | ||
|
239cf0e5f5 | ||
|
fa187665b3 | ||
|
cd0c5e954c | ||
|
490b457738 | ||
|
4d6dec9b5a | ||
|
0b11b573d9 | ||
|
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 |
22
.github/workflows/cla.yml
vendored
Normal file
22
.github/workflows/cla.yml
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
name: Contributor License Agreement (CLA)
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
cla:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
(github.event.issue.pull_request
|
||||
&& !github.event.issue.pull_request.merged_at
|
||||
&& contains(github.event.comment.body, 'signed')
|
||||
)
|
||||
|| (github.event.pull_request && !github.event.pull_request.merged)
|
||||
steps:
|
||||
- uses: Shopify/shopify-cla-action@v1
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
cla-token: ${{ secrets.CLA_TOKEN }}
|
35
.github/workflows/liquid.yml
vendored
Normal file
35
.github/workflows/liquid.yml
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
name: Liquid
|
||||
on: [push, pull_request]
|
||||
|
||||
env:
|
||||
BUNDLE_JOBS: 4
|
||||
BUNDLE_RETRY: 3
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
entry:
|
||||
- { ruby: 2.7, allowed-failure: false } # minimum supported
|
||||
- { ruby: 3.2, allowed-failure: false } # latest
|
||||
- { ruby: ruby-head, allowed-failure: true }
|
||||
name: Test Ruby ${{ matrix.entry.ruby }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: ${{ matrix.entry.ruby }}
|
||||
bundler-cache: true
|
||||
- run: bundle exec rake
|
||||
continue-on-error: ${{ matrix.entry.allowed-failure }}
|
||||
|
||||
memory_profile:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 2.7
|
||||
bundler-cache: true
|
||||
- run: bundle exec rake memory_profile:run
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -6,3 +6,5 @@ pkg
|
||||
.rvmrc
|
||||
.ruby-version
|
||||
Gemfile.lock
|
||||
.bundle
|
||||
.byebug_history
|
||||
|
29
.rubocop.yml
Normal file
29
.rubocop.yml
Normal file
@ -0,0 +1,29 @@
|
||||
inherit_gem:
|
||||
rubocop-shopify: rubocop.yml
|
||||
|
||||
inherit_from:
|
||||
- .rubocop_todo.yml
|
||||
|
||||
require: rubocop-performance
|
||||
|
||||
Performance:
|
||||
Enabled: true
|
||||
|
||||
AllCops:
|
||||
TargetRubyVersion: 2.7
|
||||
NewCops: disable
|
||||
SuggestExtensions: false
|
||||
Exclude:
|
||||
- 'vendor/bundle/**/*'
|
||||
|
||||
Naming/MethodName:
|
||||
Exclude:
|
||||
- 'example/server/liquid_servlet.rb'
|
||||
|
||||
Style/ClassMethodsDefinitions:
|
||||
Enabled: false
|
||||
|
||||
# liquid filter calls were being mistaken to be calls on arrays
|
||||
Style/ConcatArrayLiterals:
|
||||
Exclude:
|
||||
- 'test/integration/standard_filter_test.rb'
|
180
.rubocop_todo.yml
Normal file
180
.rubocop_todo.yml
Normal file
@ -0,0 +1,180 @@
|
||||
# This configuration was generated by
|
||||
# `rubocop --auto-gen-config`
|
||||
# on 2022-05-18 19:25:47 UTC using RuboCop version 1.29.1.
|
||||
# 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: 1
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
# Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation, Include.
|
||||
# Include: **/*.gemspec
|
||||
Gemspec/OrderedDependencies:
|
||||
Exclude:
|
||||
- 'liquid.gemspec'
|
||||
|
||||
# Offense count: 6
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
Layout/ClosingHeredocIndentation:
|
||||
Exclude:
|
||||
- 'test/integration/tags/for_tag_test.rb'
|
||||
|
||||
# Offense count: 34
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
Layout/EmptyLineAfterGuardClause:
|
||||
Exclude:
|
||||
- 'lib/liquid/block.rb'
|
||||
- 'lib/liquid/block_body.rb'
|
||||
- 'lib/liquid/context.rb'
|
||||
- 'lib/liquid/drop.rb'
|
||||
- 'lib/liquid/lexer.rb'
|
||||
- 'lib/liquid/parser.rb'
|
||||
- 'lib/liquid/profiler/hooks.rb'
|
||||
- 'lib/liquid/standardfilters.rb'
|
||||
- 'lib/liquid/tags/for.rb'
|
||||
- 'lib/liquid/tags/if.rb'
|
||||
- 'lib/liquid/utils.rb'
|
||||
- 'lib/liquid/variable.rb'
|
||||
- 'lib/liquid/variable_lookup.rb'
|
||||
- 'performance/shopify/money_filter.rb'
|
||||
- 'performance/shopify/paginate.rb'
|
||||
|
||||
# Offense count: 8
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
# Configuration parameters: AllowAliasSyntax, AllowedMethods.
|
||||
# AllowedMethods: alias_method, public, protected, private
|
||||
Layout/EmptyLinesAroundAttributeAccessor:
|
||||
Exclude:
|
||||
- 'lib/liquid/template.rb'
|
||||
- 'test/integration/filter_test.rb'
|
||||
- 'test/integration/tags/include_tag_test.rb'
|
||||
- 'test/unit/strainer_template_unit_test.rb'
|
||||
|
||||
# Offense count: 17
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
# Configuration parameters: EnforcedStyle, IndentationWidth.
|
||||
# SupportedStyles: aligned, indented
|
||||
Layout/LineEndStringConcatenationIndentation:
|
||||
Exclude:
|
||||
- 'test/integration/tags/for_tag_test.rb'
|
||||
- 'test/integration/tags/increment_tag_test.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
# Configuration parameters: EnforcedStyle, IndentationWidth.
|
||||
# SupportedStyles: aligned, indented
|
||||
Layout/MultilineOperationIndentation:
|
||||
Exclude:
|
||||
- 'lib/liquid/expression.rb'
|
||||
|
||||
# Offense count: 9
|
||||
Lint/MissingSuper:
|
||||
Exclude:
|
||||
- 'lib/liquid/forloop_drop.rb'
|
||||
- 'lib/liquid/tablerowloop_drop.rb'
|
||||
- 'test/integration/assign_test.rb'
|
||||
- 'test/integration/context_test.rb'
|
||||
- 'test/integration/filter_test.rb'
|
||||
- 'test/integration/standard_filter_test.rb'
|
||||
- 'test/integration/tags/for_tag_test.rb'
|
||||
- 'test/integration/tags/table_row_test.rb'
|
||||
|
||||
# Offense count: 44
|
||||
Naming/ConstantName:
|
||||
Exclude:
|
||||
- 'lib/liquid.rb'
|
||||
- 'lib/liquid/block_body.rb'
|
||||
- 'lib/liquid/tags/assign.rb'
|
||||
- 'lib/liquid/tags/capture.rb'
|
||||
- 'lib/liquid/tags/case.rb'
|
||||
- 'lib/liquid/tags/cycle.rb'
|
||||
- 'lib/liquid/tags/for.rb'
|
||||
- 'lib/liquid/tags/if.rb'
|
||||
- 'lib/liquid/tags/raw.rb'
|
||||
- 'lib/liquid/tags/table_row.rb'
|
||||
- 'lib/liquid/variable.rb'
|
||||
- 'performance/shopify/comment_form.rb'
|
||||
- 'performance/shopify/paginate.rb'
|
||||
- 'test/integration/tags/include_tag_test.rb'
|
||||
|
||||
# Offense count: 9
|
||||
# Configuration parameters: CheckIdentifiers, CheckConstants, CheckVariables, CheckStrings, CheckSymbols, CheckComments, CheckFilepaths, FlaggedTerms.
|
||||
Naming/InclusiveLanguage:
|
||||
Exclude:
|
||||
- 'lib/liquid/drop.rb'
|
||||
- 'lib/liquid/parse_context.rb'
|
||||
- 'test/integration/drop_test.rb'
|
||||
- 'test/integration/tags/if_else_tag_test.rb'
|
||||
|
||||
# Offense count: 2
|
||||
Style/ClassVars:
|
||||
Exclude:
|
||||
- 'lib/liquid/condition.rb'
|
||||
|
||||
# Offense count: 3
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
Style/ExplicitBlockArgument:
|
||||
Exclude:
|
||||
- 'test/integration/context_test.rb'
|
||||
- 'test/integration/tag/disableable_test.rb'
|
||||
- 'test/integration/tags/for_tag_test.rb'
|
||||
|
||||
# Offense count: 2982
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
# Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline.
|
||||
# SupportedStyles: single_quotes, double_quotes
|
||||
Style/StringLiterals:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 20
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
# Configuration parameters: EnforcedStyle.
|
||||
# SupportedStyles: single_quotes, double_quotes
|
||||
Style/StringLiteralsInInterpolation:
|
||||
Exclude:
|
||||
- 'lib/liquid/condition.rb'
|
||||
- 'lib/liquid/strainer_template.rb'
|
||||
- 'lib/liquid/tag/disableable.rb'
|
||||
- 'performance/shopify/shop_filter.rb'
|
||||
- 'performance/shopify/tag_filter.rb'
|
||||
|
||||
# Offense count: 6
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
# Configuration parameters: EnforcedStyleForMultiline.
|
||||
# SupportedStylesForMultiline: comma, consistent_comma, no_comma
|
||||
Style/TrailingCommaInArrayLiteral:
|
||||
Exclude:
|
||||
- 'example/server/example_servlet.rb'
|
||||
- 'lib/liquid/condition.rb'
|
||||
- 'test/integration/context_test.rb'
|
||||
- 'test/integration/standard_filter_test.rb'
|
||||
- 'test/unit/parse_tree_visitor_test.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
# Configuration parameters: EnforcedStyleForMultiline.
|
||||
# SupportedStylesForMultiline: comma, consistent_comma, no_comma
|
||||
Style/TrailingCommaInHashLiteral:
|
||||
Exclude:
|
||||
- 'lib/liquid/expression.rb'
|
||||
|
||||
# Offense count: 19
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
# Configuration parameters: EnforcedStyle, MinSize, WordRegex.
|
||||
# SupportedStyles: percent, brackets
|
||||
Style/WordArray:
|
||||
Exclude:
|
||||
- 'lib/liquid/tags/if.rb'
|
||||
- 'liquid.gemspec'
|
||||
- 'test/integration/assign_test.rb'
|
||||
- 'test/integration/context_test.rb'
|
||||
- 'test/integration/drop_test.rb'
|
||||
- 'test/integration/standard_filter_test.rb'
|
||||
|
||||
# Offense count: 117
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, IgnoredPatterns.
|
||||
# URISchemes: http, https
|
||||
Layout/LineLength:
|
||||
Max: 260
|
16
.travis.yml
16
.travis.yml
@ -1,16 +0,0 @@
|
||||
rvm:
|
||||
- 1.9
|
||||
- 2.0
|
||||
- 2.1
|
||||
- jruby-19mode
|
||||
- jruby-head
|
||||
- rbx-2
|
||||
matrix:
|
||||
allow_failures:
|
||||
- rvm: rbx-2
|
||||
- rvm: jruby-head
|
||||
|
||||
script: "rake test"
|
||||
|
||||
notifications:
|
||||
disable: true
|
@ -4,23 +4,25 @@
|
||||
|
||||
* 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
|
||||
* Documentation updates that are concise and 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
|
||||
* Documentation changes that are verbose, incorrect or not important to most people (we want to keep it simple and easy to understand)
|
||||
|
||||
## Workflow
|
||||
|
||||
* [Sign the CLA](https://cla.shopify.com/) if you haven't already
|
||||
* 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
|
||||
* For updating [Liquid documentation](https://shopify.github.io/liquid/), create it from `gh-pages` branch. (You can skip tests.)
|
||||
* If it makes sense, add tests for your code and/or run a performance benchmark
|
||||
* 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.
|
||||
|
||||
|
25
Gemfile
25
Gemfile
@ -1,9 +1,28 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
source 'https://rubygems.org'
|
||||
git_source(:github) do |repo_name|
|
||||
"https://github.com/#{repo_name}.git"
|
||||
end
|
||||
|
||||
gemspec
|
||||
gem 'stackprof', platforms: :mri_21
|
||||
|
||||
group :benchmark, :test do
|
||||
gem 'benchmark-ips'
|
||||
gem 'memory_profiler'
|
||||
gem 'terminal-table'
|
||||
|
||||
install_if -> { RUBY_PLATFORM !~ /mingw|mswin|java/ && RUBY_ENGINE != 'truffleruby' } do
|
||||
gem 'stackprof'
|
||||
end
|
||||
end
|
||||
|
||||
group :test do
|
||||
gem 'spy', '0.4.1'
|
||||
gem 'benchmark-ips'
|
||||
gem 'rubocop', '~> 1.44.0'
|
||||
gem 'rubocop-shopify', '~> 2.12.0', require: false
|
||||
gem 'rubocop-performance', require: false
|
||||
|
||||
platform :mri, :truffleruby do
|
||||
gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'master'
|
||||
end
|
||||
end
|
||||
|
332
History.md
332
History.md
@ -1,47 +1,259 @@
|
||||
# Liquid Version History
|
||||
# Liquid Change Log
|
||||
|
||||
## 3.0.0 / not yet released / branch "master"
|
||||
## 5.4.0 2022-07-29
|
||||
|
||||
* ...
|
||||
* Block parsing moved to BlockBody class, see #458 [Dylan Thacker-Smith, dylanahsmith]
|
||||
* Removed Block#end_tag. Instead, override parse with `super` followed by your code. See #446 [Dylan Thacker-Smith, dylanahsmith]
|
||||
* Fixed condition with wrong data types, see #423 [Bogdan Gusiev]
|
||||
* Add url_encode to standard filters, see #421 [Derrick Reimer, djreimer]
|
||||
* Add uniq to standard filters [Florian Weingarten, fw42]
|
||||
* Add exception_handler feature, see #397 and #254 [Bogdan Gusiev, bogdan and Florian Weingarten, fw42]
|
||||
* 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]
|
||||
### Breaking Changes
|
||||
* Drop support for end-of-life Ruby versions (2.5 and 2.6) (#1578) [Andy Waite]
|
||||
|
||||
## 2.6.1 / 2014-01-10 / branch "2-6-stable"
|
||||
### Features
|
||||
* Allow `#` to be used as an inline comment tag (#1498) [CP Clermont]
|
||||
|
||||
### Fixes
|
||||
* `PartialCache` now shares snippet cache with subcontexts by default (#1553) [Chris AtLee]
|
||||
* Hash registers no longer leak into subcontexts as static registers (#1564) [Chris AtLee]
|
||||
* Fix `ParseTreeVisitor` for `with` variable expressions in `Render` tag (#1596) [CP Clermont]
|
||||
|
||||
### Changed
|
||||
* Liquid::Context#registers now always returns a Liquid::Registers object, though supports the most used Hash functions for compatibility (#1553)
|
||||
|
||||
## 5.3.0 2022-03-22
|
||||
|
||||
### Fixes
|
||||
* StandardFilter: Fix missing @context on iterations (#1525) [Thierry Joyal]
|
||||
* Fix warning about block and default value in `static_registers.rb` (#1531) [Peter Zhu]
|
||||
|
||||
### Deprecation
|
||||
* Condition#evaluate to require mandatory context argument in Liquid 6.0.0 (#1527) [Thierry Joyal]
|
||||
|
||||
## 5.2.0 2022-03-01
|
||||
|
||||
### Features
|
||||
* Add `remove_last`, and `replace_last` filters (#1422) [Anders Hagbard]
|
||||
* Eagerly cache global filters (#1524) [Jean Boussier]
|
||||
|
||||
### Fixes
|
||||
* Fix some internal errors in filters from invalid input (#1476) [Dylan Thacker-Smith]
|
||||
* Allow dash in filter kwarg name for consistency with Liquid::C (#1518) [CP Clermont]
|
||||
|
||||
## 5.1.0 / 2021-09-09
|
||||
|
||||
### Features
|
||||
* Add `base64_encode`, `base64_decode`, `base64_url_safe_encode`, and `base64_url_safe_decode` filters (#1450) [Daniel Insley]
|
||||
* Introduce `to_liquid_value` in `Liquid::Drop` (#1441) [Michael Go]
|
||||
|
||||
### Fixes
|
||||
* Fix support for using a String subclass for the liquid source (#1421) [Dylan Thacker-Smith]
|
||||
* Add `ParseTreeVisitor` to `RangeLookup` (#1470) [CP Clermont]
|
||||
* Translate `RangeError` to `Liquid::Error` for `truncatewords` with large int (#1431) [Dylan Thacker-Smith]
|
||||
|
||||
## 5.0.1 / 2021-03-24
|
||||
|
||||
### Fixes
|
||||
* Add ParseTreeVisitor to Echo tag (#1414) [CP Clermont]
|
||||
* Test with ruby 3.0 as the latest ruby version (#1398) [Dylan Thacker-Smith]
|
||||
* Handle carriage return in newlines_to_br (#1391) [Unending]
|
||||
|
||||
### Performance Improvements
|
||||
* Use split limit in truncatewords (#1361) [Dylan Thacker-Smith]
|
||||
|
||||
## 5.0.0 / 2021-01-06
|
||||
|
||||
### Features
|
||||
* Add new `{% render %}` tag (#1122) [Samuel Doiron]
|
||||
* Add support for `as` in `{% render %}` and `{% include %}` (#1181) [Mike Angell]
|
||||
* Add `{% liquid %}` and `{% echo %}` tags (#1086) [Justin Li]
|
||||
* Add [usage tracking](README.md#usage-tracking) [Mike Angell]
|
||||
* Add `Tag.disable_tags` for disabling tags that prepend `Tag::Disableable` at render time (#1162, #1274, #1275) [Mike Angell]
|
||||
* Support using a profiler for multiple renders (#1365, #1366) [Dylan Thacker-Smith]
|
||||
|
||||
### Fixes
|
||||
* Fix catastrophic backtracking in `RANGES_REGEX` regular expression (#1357) [Dylan Thacker-Smith]
|
||||
* Make sure the for tag's limit and offset are integers (#1094) [David Cornu]
|
||||
* Invokable methods for enumerable reject include (#1151) [Thierry Joyal]
|
||||
* Allow `default` filter to handle `false` as value (#1144) [Mike Angell]
|
||||
* Fix render length resource limit so it doesn't multiply nested output (#1285) [Dylan Thacker-Smith]
|
||||
* Fix duplication of text in raw tags (#1304) [Peter Zhu]
|
||||
* Fix strict parsing of find variable with a name expression (#1317) [Dylan Thacker-Smith]
|
||||
* Use monotonic time to measure durations in Liquid::Profiler (#1362) [Dylan Thacker-Smith]
|
||||
|
||||
### Breaking Changes
|
||||
* Require Ruby >= 2.5 (#1131, #1310) [Mike Angell, Dylan Thacker-Smith]
|
||||
* Remove support for taint checking (#1268) [Dylan Thacker-Smith]
|
||||
* Split Strainer class into StrainerFactory and StrainerTemplate (#1208) [Thierry Joyal]
|
||||
* Remove handling of a nil context in the Strainer class (#1218) [Thierry Joyal]
|
||||
* Handle `BlockBody#blank?` at parse time (#1287) [Dylan Thacker-Smith]
|
||||
* Pass the tag markup and tokenizer to `Document#unknown_tag` (#1290) [Dylan Thacker-Smith]
|
||||
* And several internal changes
|
||||
|
||||
### Performance Improvements
|
||||
* Reduce allocations (#1073, #1091, #1115, #1099, #1117, #1141, #1322, #1341) [Richard Monette, Florian Weingarten, Ashwin Maroli]
|
||||
* Improve resources limits performance (#1093, #1323) [Florian Weingarten, Dylan Thacker-Smith]
|
||||
|
||||
## 4.0.3 / 2019-03-12
|
||||
|
||||
### Fixed
|
||||
* Fix break and continue tags inside included templates in loops (#1072) [Justin Li]
|
||||
|
||||
## 4.0.2 / 2019-03-08
|
||||
|
||||
### Changed
|
||||
* Add `where` filter (#1026) [Samuel Doiron]
|
||||
* Add `ParseTreeVisitor` to iterate the Liquid AST (#1025) [Stephen Paul Weber]
|
||||
* Improve `strip_html` performance (#1032) [printercu]
|
||||
|
||||
### Fixed
|
||||
* Add error checking for invalid combinations of inputs to sort, sort_natural, where, uniq, map, compact filters (#1059) [Garland Zhang]
|
||||
* Validate the character encoding in url_decode (#1070) [Clayton Smith]
|
||||
|
||||
## 4.0.1 / 2018-10-09
|
||||
|
||||
### Changed
|
||||
* Add benchmark group in Gemfile (#855) [Jerry Liu]
|
||||
* Allow benchmarks to benchmark render by itself (#851) [Jerry Liu]
|
||||
* Avoid calling `line_number` on String node when rescuing a render error. (#860) [Dylan Thacker-Smith]
|
||||
* Avoid duck typing to detect whether to call render on a node. [Dylan Thacker-Smith]
|
||||
* Clarify spelling of `reversed` on `for` block tag (#843) [Mark Crossfield]
|
||||
* Replace recursion with loop to avoid potential stack overflow from malicious input (#891, #892) [Dylan Thacker-Smith]
|
||||
* Limit block tag nesting to 100 (#894) [Dylan Thacker-Smith]
|
||||
* Replace `assert_equal nil` with `assert_nil` (#895) [Dylan Thacker-Smith]
|
||||
* Remove Spy Gem (#896) [Dylan Thacker-Smith]
|
||||
* Add `collection_name` and `variable_name` reader to `For` block (#909)
|
||||
* Symbols render as strings (#920) [Justin Li]
|
||||
* Remove default value from Hash objects (#932) [Maxime Bedard]
|
||||
* Remove one level of nesting (#944) [Dylan Thacker-Smith]
|
||||
* Update Rubocop version (#952) [Justin Li]
|
||||
* Add `at_least` and `at_most` filters (#954, #958) [Nithin Bekal]
|
||||
* Add a regression test for a liquid-c trim mode bug (#972) [Dylan Thacker-Smith]
|
||||
* Use https rather than git protocol to fetch liquid-c [Dylan Thacker-Smith]
|
||||
* Add tests against Ruby 2.4 (#963) and 2.5 (#981)
|
||||
* Replace RegExp literals with constants (#988) [Ashwin Maroli]
|
||||
* Replace unnecessary `#each_with_index` with `#each` (#992) [Ashwin Maroli]
|
||||
* Improve the unexpected end delimiter message for block tags. (#1003) [Dylan Thacker-Smith]
|
||||
* Refactor and optimize rendering (#1005) [Christopher Aue]
|
||||
* Add installation instruction (#1006) [Ben Gift]
|
||||
* Remove Circle CI (#1010)
|
||||
* Rename deprecated `BigDecimal.new` to `BigDecimal` (#1024) [Koichi ITO]
|
||||
* Rename deprecated Rubocop name (#1027) [Justin Li]
|
||||
|
||||
### Fixed
|
||||
* Handle `join` filter on non String joiners (#857) [Richard Monette]
|
||||
* Fix duplicate inclusion condition logic error of `Liquid::Strainer.add_filter` method (#861)
|
||||
* Fix `escape`, `url_encode`, `url_decode` not handling non-string values (#898) [Thierry Joyal]
|
||||
* Fix raise when variable is defined but nil when using `strict_variables` [Pascal Betz]
|
||||
* Fix `sort` and `sort_natural` to handle arrays with nils (#930) [Eric Chan]
|
||||
|
||||
## 4.0.0 / 2016-12-14 / branch "4-0-stable"
|
||||
|
||||
### Changed
|
||||
* Render an opaque internal error by default for non-Liquid::Error (#835) [Dylan Thacker-Smith]
|
||||
* Ruby 2.0 support dropped (#832) [Dylan Thacker-Smith]
|
||||
* Add to_number Drop method to allow custom drops to work with number filters (#731)
|
||||
* Add strict_variables and strict_filters options to detect undefined references (#691)
|
||||
* Improve loop performance (#681) [Florian Weingarten]
|
||||
* Rename Drop method `before_method` to `liquid_method_missing` (#661) [Thierry Joyal]
|
||||
* Add url_decode filter to invert url_encode (#645) [Larry Archer]
|
||||
* Add global_filter to apply a filter to all output (#610) [Loren Hale]
|
||||
* Add compact filter (#600) [Carson Reinke]
|
||||
* 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 `liquid_methods` (See https://github.com/Shopify/liquid/pull/568 for replacement)
|
||||
* Liquid::Template.register_filter raises when the module overrides registered public methods as private or protected (#705) [Gaurav Chande]
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fix variable names being detected as an operator when starting with contains (#788) [Michael Angell]
|
||||
* Fix include tag used with strict_variables (#828) [QuickPay]
|
||||
* Fix map filter when value is a Proc (#672) [Guillaume Malette]
|
||||
* Fix truncate filter when value is not a string (#672) [Guillaume Malette]
|
||||
* Fix behaviour of escape filter when input is nil (#665) [Tanel Jakobsoo]
|
||||
* Fix sort filter behaviour with empty array input (#652) [Marcel Cary]
|
||||
* Fix test failure under certain timezones (#631) [Dylan Thacker-Smith]
|
||||
* Fix bug in uniq filter (#595) [Florian Weingarten]
|
||||
* Fix bug 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, see #273 [Bouke van der Bijl, bouk]
|
||||
* Prevent arbitrary method invocation on condition objects, see #274 [Dylan Thacker-Smith, dylanahsmith]
|
||||
* 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
|
||||
|
||||
@ -49,37 +261,37 @@ IMPORTANT: Liquid 2.6 is going to be the last version of Liquid which maintains
|
||||
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.5 / 2014-01-10 / branch "2-5-stable"
|
||||
|
||||
Security fix, cherry-picked from master (4e14a65):
|
||||
* Don't call to_sym when creating conditions for security reasons, see #273 [Bouke van der Bijl, bouk]
|
||||
* Prevent arbitrary method invocation on condition objects, see #274 [Dylan Thacker-Smith, dylanahsmith]
|
||||
* 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", see #173, [wǒ_is神仙, jsw0528]
|
||||
* 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
|
||||
|
||||
@ -87,7 +299,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
|
||||
|
||||
|
47
README.md
47
README.md
@ -5,7 +5,7 @@
|
||||
|
||||
* [Contributing guidelines](CONTRIBUTING.md)
|
||||
* [Version history](History.md)
|
||||
* [Liquid documentation from Shopify](http://docs.shopify.com/themes/liquid-basics)
|
||||
* [Liquid documentation from Shopify](https://shopify.dev/docs/api/liquid)
|
||||
* [Liquid Wiki at GitHub](https://github.com/Shopify/liquid/wiki)
|
||||
* [Website](http://liquidmarkup.org/)
|
||||
|
||||
@ -42,6 +42,8 @@ Liquid is a template engine which was written with very specific requirements:
|
||||
|
||||
## How to use Liquid
|
||||
|
||||
Install Liquid by adding `gem 'liquid'` to your gemfile.
|
||||
|
||||
Liquid supports a very simple API based around the Liquid::Template class.
|
||||
For standard use you can just pass it the content of a file and call render with a parameters hash.
|
||||
|
||||
@ -54,22 +56,59 @@ For standard use you can just pass it the content of a file and call render with
|
||||
|
||||
Setting the error mode of Liquid lets you specify how strictly you want your templates to be interpreted.
|
||||
Normally the parser is very lax and will accept almost anything without error. Unfortunately this can make
|
||||
it very hard to debug and can lead to unexpected behaviour.
|
||||
it very hard to debug and can lead to unexpected behaviour.
|
||||
|
||||
Liquid also comes with a stricter parser that can be used when editing templates to give better error messages
|
||||
when templates are invalid. You can enable this new parser like this:
|
||||
|
||||
```ruby
|
||||
Liquid::Template.error_mode = :strict # Raises a SyntaxError when invalid syntax is used
|
||||
Liquid::Template.error_mode = :warn # Adds errors to template.errors but continues as normal
|
||||
Liquid::Template.error_mode = :warn # Adds strict errors to template.errors but continues as normal
|
||||
Liquid::Template.error_mode = :lax # The default mode, accepts almost anything.
|
||||
```
|
||||
|
||||
If you want to set the error mode only on specific templates you can pass `:error_mode` as an option to `parse`:
|
||||
```ruby
|
||||
Liquid::Template.parse(source, :error_mode => :strict)
|
||||
Liquid::Template.parse(source, error_mode: :strict)
|
||||
```
|
||||
This is useful for doing things like enabling strict mode only in the theme editor.
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
### Usage tracking
|
||||
|
||||
To help track usages of a feature or code path in production, we have released opt-in usage tracking. To enable this, we provide an empty `Liquid:: Usage.increment` method which you can customize to your needs. The feature is well suited to https://github.com/Shopify/statsd-instrument. However, the choice of implementation is up to you.
|
||||
|
||||
Once you have enabled usage tracking, we recommend reporting any events through Github Issues that your system may be logging. It is highly likely this event has been added to consider deprecating or improving code specific to this event, so please raise any concerns.
|
||||
|
65
Rakefile
65
Rakefile
@ -1,42 +1,70 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rake'
|
||||
require 'rake/testtask'
|
||||
$LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
|
||||
$LOAD_PATH.unshift(File.expand_path("../lib", __FILE__))
|
||||
require "liquid/version"
|
||||
|
||||
task :default => 'test'
|
||||
task(default: [:test, :rubocop])
|
||||
|
||||
desc 'run test suite with default parser'
|
||||
desc('run test suite with default parser')
|
||||
Rake::TestTask.new(:base_test) do |t|
|
||||
t.libs << '.' << 'lib' << 'test'
|
||||
t.libs << 'lib' << 'test'
|
||||
t.test_files = FileList['test/{integration,unit}/**/*_test.rb']
|
||||
t.verbose = false
|
||||
t.verbose = false
|
||||
end
|
||||
|
||||
desc 'run test suite with warn error mode'
|
||||
Rake::TestTask.new(:integration_test) do |t|
|
||||
t.libs << 'lib' << 'test'
|
||||
t.test_files = FileList['test/integration/**/*_test.rb']
|
||||
t.verbose = false
|
||||
end
|
||||
|
||||
desc('run test suite with warn error mode')
|
||||
task :warn_test do
|
||||
ENV['LIQUID_PARSER_MODE'] = 'warn'
|
||||
Rake::Task['base_test'].invoke
|
||||
end
|
||||
|
||||
desc 'runs test suite with both strict and lax parsers'
|
||||
task :rubocop do
|
||||
if RUBY_ENGINE == 'ruby'
|
||||
require 'rubocop/rake_task'
|
||||
RuboCop::RakeTask.new
|
||||
end
|
||||
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' || RUBY_ENGINE == 'truffleruby'
|
||||
ENV['LIQUID_C'] = '1'
|
||||
|
||||
ENV['LIQUID_PARSER_MODE'] = 'lax'
|
||||
Rake::Task['integration_test'].reenable
|
||||
Rake::Task['integration_test'].invoke
|
||||
|
||||
ENV['LIQUID_PARSER_MODE'] = 'strict'
|
||||
Rake::Task['integration_test'].reenable
|
||||
Rake::Task['integration_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 +72,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 +83,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 +93,20 @@ namespace :profile do
|
||||
task :strict do
|
||||
ruby "./performance/profile.rb strict"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
desc "Run example"
|
||||
namespace :memory_profile do
|
||||
desc "Run memory profiler"
|
||||
task :run do
|
||||
ruby "./performance/memory_profile.rb"
|
||||
end
|
||||
end
|
||||
|
||||
desc("Run example")
|
||||
task :example do
|
||||
ruby "-w -d -Ilib example/server/server.rb"
|
||||
end
|
||||
|
||||
task :console do
|
||||
exec 'irb -I lib -r liquid'
|
||||
end
|
||||
|
@ -1,10 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ProductsFilter
|
||||
def price(integer)
|
||||
sprintf("$%.2d USD", integer / 100.0)
|
||||
format("$%.2d USD", integer / 100.0)
|
||||
end
|
||||
|
||||
def prettyprint(text)
|
||||
text.gsub( /\*(.*)\*/, '<b>\1</b>' )
|
||||
text.gsub(/\*(.*)\*/, '<b>\1</b>')
|
||||
end
|
||||
|
||||
def count(array)
|
||||
@ -17,25 +19,32 @@ 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,6 @@
|
||||
class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet
|
||||
# frozen_string_literal: true
|
||||
|
||||
class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet
|
||||
def do_GET(req, res)
|
||||
handle(:get, req, res)
|
||||
end
|
||||
@ -10,20 +11,20 @@ class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet
|
||||
|
||||
private
|
||||
|
||||
def handle(type, req, res)
|
||||
@request = req
|
||||
def handle(_type, req, res)
|
||||
@request = req
|
||||
@response = res
|
||||
|
||||
@request.path_info =~ /(\w+)\z/
|
||||
@action = $1 || 'index'
|
||||
@action = Regexp.last_match(1) || 'index'
|
||||
@assigns = send(@action) if respond_to?(@action)
|
||||
|
||||
@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,14 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
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 }
|
||||
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 }}
|
||||
|
@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Copyright (c) 2005 Tobias Luetke
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
@ -21,11 +23,13 @@
|
||||
|
||||
module Liquid
|
||||
FilterSeparator = /\|/
|
||||
ArgumentSeparator = ','.freeze
|
||||
FilterArgumentSeparator = ':'.freeze
|
||||
VariableAttributeSeparator = '.'.freeze
|
||||
ArgumentSeparator = ','
|
||||
FilterArgumentSeparator = ':'
|
||||
VariableAttributeSeparator = '.'
|
||||
WhitespaceControl = '-'
|
||||
TagStart = /\{\%/
|
||||
TagEnd = /\%\}/
|
||||
TagName = /#|\w+/
|
||||
VariableSignature = /\(?[\w\-\.\[\]]\)?/
|
||||
VariableSegment = /[\w\-]/
|
||||
VariableStart = /\{\{/
|
||||
@ -33,29 +37,37 @@ module Liquid
|
||||
VariableIncompleteEnd = /\}\}?/
|
||||
QuotedString = /"[^"]*"|'[^']*'/
|
||||
QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o
|
||||
TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/o
|
||||
AnyStartingTag = /\{\{|\{\%/
|
||||
TagAttributes = /(\w[\w-]*)\s*\:\s*(#{QuotedFragment})/o
|
||||
AnyStartingTag = /#{TagStart}|#{VariableStart}/o
|
||||
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om
|
||||
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om
|
||||
VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o
|
||||
VariableParser = /\[(?>[^\[\]]+|\g<0>)*\]|#{VariableSegment}+\??/o
|
||||
|
||||
RAISE_EXCEPTION_LAMBDA = ->(_e) { raise }
|
||||
|
||||
singleton_class.send(:attr_accessor, :cache_classes)
|
||||
self.cache_classes = true
|
||||
end
|
||||
|
||||
require "liquid/version"
|
||||
require 'liquid/parse_tree_visitor'
|
||||
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/strainer_template'
|
||||
require 'liquid/strainer_factory'
|
||||
require 'liquid/expression'
|
||||
require 'liquid/context'
|
||||
require 'liquid/parser_switching'
|
||||
require 'liquid/tag'
|
||||
require 'liquid/tag/disabler'
|
||||
require 'liquid/tag/disableable'
|
||||
require 'liquid/block'
|
||||
require 'liquid/block_body'
|
||||
require 'liquid/document'
|
||||
@ -63,16 +75,18 @@ 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/token'
|
||||
require 'liquid/tokenizer'
|
||||
require 'liquid/parse_context'
|
||||
require 'liquid/partial_cache'
|
||||
require 'liquid/usage'
|
||||
require 'liquid/registers'
|
||||
require 'liquid/template_factory'
|
||||
|
||||
# Load all the tags of the standard library
|
||||
#
|
||||
Dir[File.dirname(__FILE__) + '/liquid/tags/*.rb'].each { |f| require f }
|
||||
|
||||
require 'liquid/profiler'
|
||||
require 'liquid/profiler/hooks'
|
||||
Dir["#{__dir__}/liquid/tags/*.rb"].each { |f| require f }
|
||||
|
@ -1,16 +1,22 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Block < Tag
|
||||
MAX_DEPTH = 100
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
@blank = true
|
||||
end
|
||||
|
||||
def parse(tokens)
|
||||
@body = BlockBody.new
|
||||
while more = parse_body(@body, tokens)
|
||||
@body = new_body
|
||||
while parse_body(@body, tokens)
|
||||
end
|
||||
@body.freeze
|
||||
end
|
||||
|
||||
# For backwards compatibility
|
||||
def render(context)
|
||||
@body.render(context)
|
||||
end
|
||||
@ -23,32 +29,33 @@ module Liquid
|
||||
@body.nodelist
|
||||
end
|
||||
|
||||
# warnings of this block and all sub-tags
|
||||
def warnings
|
||||
all_warnings = []
|
||||
all_warnings.concat(@warnings) if @warnings
|
||||
|
||||
(nodelist || []).each do |node|
|
||||
all_warnings.concat(node.warnings || []) if node.respond_to?(:warnings)
|
||||
end
|
||||
|
||||
all_warnings
|
||||
def unknown_tag(tag_name, _markup, _tokenizer)
|
||||
Block.raise_unknown_tag(tag_name, block_name, block_delimiter, parse_context)
|
||||
end
|
||||
|
||||
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))
|
||||
when 'end'.freeze
|
||||
raise SyntaxError.new(options[:locale].t("errors.syntax.invalid_delimiter".freeze,
|
||||
:block_name => block_name,
|
||||
:block_delimiter => block_delimiter))
|
||||
# @api private
|
||||
def self.raise_unknown_tag(tag, block_name, block_delimiter, parse_context)
|
||||
if tag == 'else'
|
||||
raise SyntaxError, parse_context.locale.t(
|
||||
"errors.syntax.unexpected_else",
|
||||
block_name: block_name,
|
||||
)
|
||||
elsif tag.start_with?('end')
|
||||
raise SyntaxError, parse_context.locale.t(
|
||||
"errors.syntax.invalid_delimiter",
|
||||
tag: tag,
|
||||
block_name: block_name,
|
||||
block_delimiter: block_delimiter,
|
||||
)
|
||||
else
|
||||
raise SyntaxError.new(options[:locale].t("errors.syntax.unknown_tag".freeze, :tag => tag))
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag)
|
||||
end
|
||||
end
|
||||
|
||||
def raise_tag_never_closed(block_name)
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_never_closed", block_name: block_name)
|
||||
end
|
||||
|
||||
def block_name
|
||||
@tag_name
|
||||
end
|
||||
@ -57,20 +64,32 @@ module Liquid
|
||||
@block_delimiter ||= "end#{block_name}"
|
||||
end
|
||||
|
||||
protected
|
||||
private
|
||||
|
||||
# @api public
|
||||
def new_body
|
||||
parse_context.new_block_body
|
||||
end
|
||||
|
||||
# @api public
|
||||
def parse_body(body, tokens)
|
||||
body.parse(tokens, options) do |end_tag_name, end_tag_params|
|
||||
@blank &&= body.blank?
|
||||
if parse_context.depth >= MAX_DEPTH
|
||||
raise StackLevelError, "Nesting too deep"
|
||||
end
|
||||
parse_context.depth += 1
|
||||
begin
|
||||
body.parse(tokens, parse_context) do |end_tag_name, end_tag_params|
|
||||
@blank &&= body.blank?
|
||||
|
||||
return false if end_tag_name == block_delimiter
|
||||
unless end_tag_name
|
||||
raise SyntaxError.new(@options[:locale].t("errors.syntax.tag_never_closed".freeze, :block_name => block_name))
|
||||
return false if end_tag_name == block_delimiter
|
||||
raise_tag_never_closed(block_name) unless end_tag_name
|
||||
|
||||
# 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
|
||||
|
||||
# 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)
|
||||
ensure
|
||||
parse_context.depth -= 1
|
||||
end
|
||||
|
||||
true
|
||||
|
@ -1,131 +1,269 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'English'
|
||||
|
||||
module Liquid
|
||||
class BlockBody
|
||||
FullToken = /\A#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
|
||||
ContentOfVariable = /\A#{VariableStart}(.*)#{VariableEnd}\z/om
|
||||
TAGSTART = "{%".freeze
|
||||
VARSTART = "{{".freeze
|
||||
LiquidTagToken = /\A\s*(#{TagName})\s*(.*?)\z/o
|
||||
FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(#{TagName})(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
|
||||
ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
|
||||
WhitespaceOrNothing = /\A\s*\z/
|
||||
TAGSTART = "{%"
|
||||
VARSTART = "{{"
|
||||
|
||||
attr_reader :nodelist
|
||||
|
||||
def initialize
|
||||
@nodelist = []
|
||||
@blank = true
|
||||
@blank = true
|
||||
end
|
||||
|
||||
def parse(tokens, options)
|
||||
while token = tokens.shift
|
||||
begin
|
||||
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 = Template.tags[tag_name]
|
||||
markup = token.child(markup) if token.is_a?(Token)
|
||||
new_tag = tag.parse(tag_name, markup, tokens, options)
|
||||
new_tag.line_number = token.line_number if token.is_a?(Token)
|
||||
@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, options)
|
||||
end
|
||||
when token.start_with?(VARSTART)
|
||||
new_var = create_variable(token, options)
|
||||
new_var.line_number = token.line_number if token.is_a?(Token)
|
||||
@nodelist << new_var
|
||||
@blank = false
|
||||
else
|
||||
@nodelist << token
|
||||
@blank &&= !!(token =~ /\A\s*\z/)
|
||||
end
|
||||
def parse(tokenizer, parse_context, &block)
|
||||
raise FrozenError, "can't modify frozen Liquid::BlockBody" if frozen?
|
||||
|
||||
parse_context.line_number = tokenizer.line_number
|
||||
|
||||
if tokenizer.for_liquid_tag
|
||||
parse_for_liquid_tag(tokenizer, parse_context, &block)
|
||||
else
|
||||
parse_for_document(tokenizer, parse_context, &block)
|
||||
end
|
||||
end
|
||||
|
||||
def freeze
|
||||
@nodelist.freeze
|
||||
super
|
||||
end
|
||||
|
||||
private def parse_for_liquid_tag(tokenizer, parse_context)
|
||||
while (token = tokenizer.shift)
|
||||
unless token.empty? || token.match?(WhitespaceOrNothing)
|
||||
unless token =~ LiquidTagToken
|
||||
# line isn't empty but didn't match tag syntax, yield and let the
|
||||
# caller raise a syntax error
|
||||
return yield token, token
|
||||
end
|
||||
rescue SyntaxError => e
|
||||
e.set_line_number_from_token(token)
|
||||
raise
|
||||
tag_name = Regexp.last_match(1)
|
||||
markup = Regexp.last_match(2)
|
||||
|
||||
if tag_name == 'liquid'
|
||||
parse_context.line_number -= 1
|
||||
next parse_liquid_tag(markup, parse_context)
|
||||
end
|
||||
|
||||
unless (tag = registered_tags[tag_name])
|
||||
# end parsing if we reach an unknown tag and let the caller decide
|
||||
# determine how to proceed
|
||||
return yield tag_name, markup
|
||||
end
|
||||
new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
|
||||
@blank &&= new_tag.blank?
|
||||
@nodelist << new_tag
|
||||
end
|
||||
parse_context.line_number = tokenizer.line_number
|
||||
end
|
||||
|
||||
yield nil, nil
|
||||
end
|
||||
|
||||
# @api private
|
||||
def self.unknown_tag_in_liquid_tag(tag, parse_context)
|
||||
Block.raise_unknown_tag(tag, 'liquid', '%}', parse_context)
|
||||
end
|
||||
|
||||
# @api private
|
||||
def self.raise_missing_tag_terminator(token, parse_context)
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_termination", token: token, tag_end: TagEnd.inspect)
|
||||
end
|
||||
|
||||
# @api private
|
||||
def self.raise_missing_variable_terminator(token, parse_context)
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.variable_termination", token: token, tag_end: VariableEnd.inspect)
|
||||
end
|
||||
|
||||
# @api private
|
||||
def self.render_node(context, output, node)
|
||||
node.render_to_output_buffer(context, output)
|
||||
rescue => exc
|
||||
blank_tag = !node.instance_of?(Variable) && node.blank?
|
||||
rescue_render_node(context, output, node.line_number, exc, blank_tag)
|
||||
end
|
||||
|
||||
# @api private
|
||||
def self.rescue_render_node(context, output, line_number, exc, blank_tag)
|
||||
case exc
|
||||
when MemoryError
|
||||
raise
|
||||
when UndefinedVariable, UndefinedDropMethod, UndefinedFilter
|
||||
context.handle_error(exc, line_number)
|
||||
else
|
||||
error_message = context.handle_error(exc, line_number)
|
||||
unless blank_tag # conditional for backwards compatibility
|
||||
output << error_message
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private def parse_liquid_tag(markup, parse_context)
|
||||
liquid_tag_tokenizer = parse_context.new_tokenizer(
|
||||
markup, start_line_number: parse_context.line_number, for_liquid_tag: true
|
||||
)
|
||||
parse_for_liquid_tag(liquid_tag_tokenizer, parse_context) do |end_tag_name, _end_tag_markup|
|
||||
if end_tag_name
|
||||
BlockBody.unknown_tag_in_liquid_tag(end_tag_name, parse_context)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private def handle_invalid_tag_token(token, parse_context)
|
||||
if token.end_with?('%}')
|
||||
yield token, token
|
||||
else
|
||||
BlockBody.raise_missing_tag_terminator(token, parse_context)
|
||||
end
|
||||
end
|
||||
|
||||
private def parse_for_document(tokenizer, parse_context, &block)
|
||||
while (token = tokenizer.shift)
|
||||
next if token.empty?
|
||||
case
|
||||
when token.start_with?(TAGSTART)
|
||||
whitespace_handler(token, parse_context)
|
||||
unless token =~ FullToken
|
||||
return handle_invalid_tag_token(token, parse_context, &block)
|
||||
end
|
||||
tag_name = Regexp.last_match(2)
|
||||
markup = Regexp.last_match(4)
|
||||
|
||||
if parse_context.line_number
|
||||
# newlines inside the tag should increase the line number,
|
||||
# particularly important for multiline {% liquid %} tags
|
||||
parse_context.line_number += Regexp.last_match(1).count("\n") + Regexp.last_match(3).count("\n")
|
||||
end
|
||||
|
||||
if tag_name == 'liquid'
|
||||
parse_liquid_tag(markup, parse_context)
|
||||
next
|
||||
end
|
||||
|
||||
unless (tag = registered_tags[tag_name])
|
||||
# end parsing if we reach an unknown tag and let the caller decide
|
||||
# determine how to proceed
|
||||
return yield tag_name, markup
|
||||
end
|
||||
new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
|
||||
@blank &&= new_tag.blank?
|
||||
@nodelist << new_tag
|
||||
when token.start_with?(VARSTART)
|
||||
whitespace_handler(token, parse_context)
|
||||
@nodelist << create_variable(token, parse_context)
|
||||
@blank = false
|
||||
else
|
||||
if parse_context.trim_whitespace
|
||||
token.lstrip!
|
||||
end
|
||||
parse_context.trim_whitespace = false
|
||||
@nodelist << token
|
||||
@blank &&= token.match?(WhitespaceOrNothing)
|
||||
end
|
||||
parse_context.line_number = tokenizer.line_number
|
||||
end
|
||||
|
||||
yield nil, nil
|
||||
end
|
||||
|
||||
def whitespace_handler(token, parse_context)
|
||||
if token[2] == WhitespaceControl
|
||||
previous_token = @nodelist.last
|
||||
if previous_token.is_a?(String)
|
||||
first_byte = previous_token.getbyte(0)
|
||||
previous_token.rstrip!
|
||||
if previous_token.empty? && parse_context[:bug_compatible_whitespace_trimming] && first_byte
|
||||
previous_token << first_byte
|
||||
end
|
||||
end
|
||||
end
|
||||
parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
|
||||
end
|
||||
|
||||
def blank?
|
||||
@blank
|
||||
end
|
||||
|
||||
def warnings
|
||||
all_warnings = []
|
||||
nodelist.each do |node|
|
||||
all_warnings.concat(node.warnings) if node.respond_to?(:warnings) && node.warnings
|
||||
end
|
||||
all_warnings
|
||||
# Remove blank strings in the block body for a control flow tag (e.g. `if`, `for`, `case`, `unless`)
|
||||
# with a blank body.
|
||||
#
|
||||
# For example, in a conditional assignment like the following
|
||||
#
|
||||
# ```
|
||||
# {% if size > max_size %}
|
||||
# {% assign size = max_size %}
|
||||
# {% endif %}
|
||||
# ```
|
||||
#
|
||||
# we assume the intention wasn't to output the blank spaces in the `if` tag's block body, so this method
|
||||
# will remove them to reduce the render output size.
|
||||
#
|
||||
# Note that it is now preferred to use the `liquid` tag for this use case.
|
||||
def remove_blank_strings
|
||||
raise "remove_blank_strings only support being called on a blank block body" unless @blank
|
||||
@nodelist.reject! { |node| node.instance_of?(String) }
|
||||
end
|
||||
|
||||
def render(context)
|
||||
output = []
|
||||
context.resource_limits[:render_length_current] = 0
|
||||
context.resource_limits[:render_score_current] += @nodelist.length
|
||||
render_to_output_buffer(context, +'')
|
||||
end
|
||||
|
||||
@nodelist.each do |token|
|
||||
# Break out if we have any unhanded interrupts.
|
||||
break if context.has_interrupt?
|
||||
def render_to_output_buffer(context, output)
|
||||
freeze unless frozen?
|
||||
|
||||
begin
|
||||
context.resource_limits.increment_render_score(@nodelist.length)
|
||||
|
||||
idx = 0
|
||||
while (node = @nodelist[idx])
|
||||
if node.instance_of?(String)
|
||||
output << node
|
||||
else
|
||||
render_node(context, output, node)
|
||||
# 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 = render_token(token, context)
|
||||
|
||||
unless token.is_a?(Block) && token.blank?
|
||||
output << token_output
|
||||
end
|
||||
rescue MemoryError => e
|
||||
raise e
|
||||
rescue ::StandardError => e
|
||||
output << context.handle_error(e, token)
|
||||
# or {% continue %}. These tags may also occur through Block or Include tags.
|
||||
break if context.interrupt? # might have happened in a for-block
|
||||
end
|
||||
idx += 1
|
||||
|
||||
context.resource_limits.increment_write_score(output)
|
||||
end
|
||||
|
||||
output.join
|
||||
output
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def render_token(token, context)
|
||||
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)
|
||||
def render_node(context, output, node)
|
||||
BlockBody.render_node(context, output, node)
|
||||
end
|
||||
|
||||
def create_variable(token, parse_context)
|
||||
if token =~ ContentOfVariable
|
||||
markup = Regexp.last_match(1)
|
||||
return Variable.new(markup, parse_context)
|
||||
end
|
||||
token_output
|
||||
BlockBody.raise_missing_variable_terminator(token, parse_context)
|
||||
end
|
||||
|
||||
def create_variable(token, options)
|
||||
token.scan(ContentOfVariable) do |content|
|
||||
markup = token.is_a?(Token) ? token.child(content.first) : content.first
|
||||
return Variable.new(markup, options)
|
||||
end
|
||||
raise_missing_variable_terminator(token, options)
|
||||
# @deprecated Use {.raise_missing_tag_terminator} instead
|
||||
def raise_missing_tag_terminator(token, parse_context)
|
||||
BlockBody.raise_missing_tag_terminator(token, parse_context)
|
||||
end
|
||||
|
||||
def raise_missing_tag_terminator(token, options)
|
||||
raise SyntaxError.new(options[:locale].t("errors.syntax.tag_termination".freeze, :token => token, :tag_end => TagEnd.inspect))
|
||||
# @deprecated Use {.raise_missing_variable_terminator} instead
|
||||
def raise_missing_variable_terminator(token, parse_context)
|
||||
BlockBody.raise_missing_variable_terminator(token, parse_context)
|
||||
end
|
||||
|
||||
def raise_missing_variable_terminator(token, options)
|
||||
raise SyntaxError.new(options[:locale].t("errors.syntax.variable_termination".freeze, :token => token, :tag_end => VariableEnd.inspect))
|
||||
def registered_tags
|
||||
Template.tags
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# Container for liquid nodes which conveniently wraps decision making logic
|
||||
#
|
||||
@ -6,55 +8,85 @@ module Liquid
|
||||
# c = Condition.new(1, '==', 1)
|
||||
# c.evaluate #=> true
|
||||
#
|
||||
class Condition #:nodoc:
|
||||
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 => :<,
|
||||
'>'.freeze => :>,
|
||||
'>='.freeze => :>=,
|
||||
'<='.freeze => :<=,
|
||||
'contains'.freeze => lambda { |cond, left, right|
|
||||
left && right && left.respond_to?(:include?) ? left.include?(right) : false
|
||||
}
|
||||
'==' => ->(cond, left, right) { cond.send(:equal_variables, left, right) },
|
||||
'!=' => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
|
||||
'<>' => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
|
||||
'<' => :<,
|
||||
'>' => :>,
|
||||
'>=' => :>=,
|
||||
'<=' => :<=,
|
||||
'contains' => 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,
|
||||
}
|
||||
|
||||
class MethodLiteral
|
||||
attr_reader :method_name, :to_s
|
||||
|
||||
def initialize(method_name, to_s)
|
||||
@method_name = method_name
|
||||
@to_s = to_s
|
||||
end
|
||||
end
|
||||
|
||||
@@method_literals = {
|
||||
'blank' => MethodLiteral.new(:blank?, '').freeze,
|
||||
'empty' => MethodLiteral.new(:empty?, '').freeze,
|
||||
}
|
||||
|
||||
def self.operators
|
||||
@@operators
|
||||
end
|
||||
|
||||
attr_reader :attachment
|
||||
def self.parse_expression(parse_context, markup)
|
||||
@@method_literals[markup] || parse_context.parse_expression(markup)
|
||||
end
|
||||
|
||||
attr_reader :attachment, :child_condition
|
||||
attr_accessor :left, :operator, :right
|
||||
|
||||
def initialize(left = nil, operator = nil, right = nil)
|
||||
@left = left
|
||||
@left = left
|
||||
@operator = operator
|
||||
@right = right
|
||||
@right = right
|
||||
|
||||
@child_relation = nil
|
||||
@child_condition = nil
|
||||
end
|
||||
|
||||
def evaluate(context = Context.new)
|
||||
result = interpret_condition(left, right, operator, context)
|
||||
def evaluate(context = deprecated_default_context)
|
||||
condition = self
|
||||
result = nil
|
||||
loop do
|
||||
result = interpret_condition(condition.left, condition.right, condition.operator, context)
|
||||
|
||||
case @child_relation
|
||||
when :or
|
||||
result || @child_condition.evaluate(context)
|
||||
when :and
|
||||
result && @child_condition.evaluate(context)
|
||||
else
|
||||
result
|
||||
case condition.child_relation
|
||||
when :or
|
||||
break if Liquid::Utils.to_liquid_value(result)
|
||||
when :and
|
||||
break unless Liquid::Utils.to_liquid_value(result)
|
||||
else
|
||||
break
|
||||
end
|
||||
condition = condition.child_condition
|
||||
end
|
||||
result
|
||||
end
|
||||
|
||||
def or(condition)
|
||||
@child_relation = :or
|
||||
@child_relation = :or
|
||||
@child_condition = condition
|
||||
end
|
||||
|
||||
def and(condition)
|
||||
@child_relation = :and
|
||||
@child_relation = :and
|
||||
@child_condition = condition
|
||||
end
|
||||
|
||||
@ -67,23 +99,27 @@ module Liquid
|
||||
end
|
||||
|
||||
def inspect
|
||||
"#<Condition #{[@left, @operator, @right].compact.join(' '.freeze)}>"
|
||||
"#<Condition #{[@left, @operator, @right].compact.join(' ')}>"
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_reader :child_relation
|
||||
|
||||
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?(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?(MethodLiteral)
|
||||
if left.respond_to?(right.method_name)
|
||||
return left.send(right.method_name)
|
||||
else
|
||||
return nil
|
||||
end
|
||||
@ -96,36 +132,49 @@ 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.evaluate(left) if op == nil
|
||||
return context.evaluate(left) if op.nil?
|
||||
|
||||
left = context.evaluate(left)
|
||||
right = context.evaluate(right)
|
||||
left = Liquid::Utils.to_liquid_value(context.evaluate(left))
|
||||
right = Liquid::Utils.to_liquid_value(context.evaluate(right))
|
||||
|
||||
operation = self.class.operators[op] || raise(Liquid::ArgumentError.new("Unknown operator #{op}"))
|
||||
operation = self.class.operators[op] || raise(Liquid::ArgumentError, "Unknown operator #{op}")
|
||||
|
||||
if operation.respond_to?(:call)
|
||||
operation.call(self, left, right)
|
||||
elsif left.respond_to?(operation) and right.respond_to?(operation)
|
||||
elsif left.respond_to?(operation) && right.respond_to?(operation) && !left.is_a?(Hash) && !right.is_a?(Hash)
|
||||
begin
|
||||
left.send(operation, right)
|
||||
rescue ::ArgumentError => e
|
||||
raise Liquid::ArgumentError.new(e.message)
|
||||
raise Liquid::ArgumentError, e.message
|
||||
end
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def deprecated_default_context
|
||||
warn("DEPRECATION WARNING: Condition#evaluate without a context argument is deprecated" \
|
||||
" and will be removed from Liquid 6.0.0.")
|
||||
Context.new
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
[
|
||||
@node.left,
|
||||
@node.right,
|
||||
@node.child_condition,
|
||||
@node.attachment
|
||||
].compact
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
class ElseCondition < Condition
|
||||
def else?
|
||||
true
|
||||
end
|
||||
|
||||
def evaluate(context)
|
||||
def evaluate(_context)
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
@ -1,5 +1,6 @@
|
||||
module Liquid
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# Context keeps the variable stack and resolves variables, as well as keywords
|
||||
#
|
||||
# context['variable'] = 'testing'
|
||||
@ -13,45 +14,53 @@ module Liquid
|
||||
#
|
||||
# context['bob'] #=> nil class Context
|
||||
class Context
|
||||
attr_reader :scopes, :errors, :registers, :environments, :resource_limits
|
||||
attr_accessor :exception_handler
|
||||
attr_reader :scopes, :errors, :registers, :environments, :resource_limits, :static_registers, :static_environments
|
||||
attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters
|
||||
|
||||
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil)
|
||||
@environments = [environments].flatten
|
||||
@scopes = [(outer_scope || {})]
|
||||
@registers = registers
|
||||
@errors = []
|
||||
@resource_limits = resource_limits || Template.default_resource_limits.dup
|
||||
@resource_limits[:render_score_current] = 0
|
||||
@resource_limits[:assign_score_current] = 0
|
||||
squash_instance_assigns_with_environments
|
||||
# rubocop:disable Metrics/ParameterLists
|
||||
def self.build(environments: {}, outer_scope: {}, registers: {}, rethrow_errors: false, resource_limits: nil, static_environments: {}, &block)
|
||||
new(environments, outer_scope, registers, rethrow_errors, resource_limits, static_environments, &block)
|
||||
end
|
||||
|
||||
@this_stack_used = false
|
||||
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil, static_environments = {})
|
||||
@environments = [environments]
|
||||
@environments.flatten!
|
||||
|
||||
@static_environments = [static_environments].flatten(1).freeze
|
||||
@scopes = [(outer_scope || {})]
|
||||
@registers = registers.is_a?(Registers) ? registers : Registers.new(registers)
|
||||
@errors = []
|
||||
@partial = false
|
||||
@strict_variables = false
|
||||
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
|
||||
@base_scope_depth = 0
|
||||
@interrupts = []
|
||||
@filters = []
|
||||
@global_filter = nil
|
||||
@disabled_tags = {}
|
||||
|
||||
@registers.static[:cached_partials] ||= {}
|
||||
@registers.static[:file_system] ||= Liquid::Template.file_system
|
||||
@registers.static[:template_factory] ||= Liquid::TemplateFactory.new
|
||||
|
||||
self.exception_renderer = Template.default_exception_renderer
|
||||
if rethrow_errors
|
||||
self.exception_handler = ->(e) { true }
|
||||
self.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA
|
||||
end
|
||||
|
||||
@interrupts = []
|
||||
@filters = []
|
||||
end
|
||||
yield self if block_given?
|
||||
|
||||
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
|
||||
# Do this last, since it could result in this object being passed to a Proc in the environment
|
||||
squash_instance_assigns_with_environments
|
||||
end
|
||||
# rubocop:enable Metrics/ParameterLists
|
||||
|
||||
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
|
||||
@strainer ||= Strainer.create(self, @filters)
|
||||
@strainer ||= StrainerFactory.create(self, @filters)
|
||||
end
|
||||
|
||||
# Adds filters to this context.
|
||||
@ -60,25 +69,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
|
||||
|
||||
@ -92,15 +92,12 @@ module Liquid
|
||||
@interrupts.pop
|
||||
end
|
||||
|
||||
|
||||
def handle_error(e, token=nil)
|
||||
if e.is_a?(Liquid::Error)
|
||||
e.set_line_number_from_token(token)
|
||||
end
|
||||
|
||||
def handle_error(e, line_number = nil)
|
||||
e = internal_error unless e.is_a?(Liquid::Error)
|
||||
e.template_name ||= template_name
|
||||
e.line_number ||= line_number
|
||||
errors.push(e)
|
||||
raise if exception_handler && exception_handler.call(e)
|
||||
Liquid::Error.render(e)
|
||||
exception_renderer.call(e).to_s
|
||||
end
|
||||
|
||||
def invoke(method, *args)
|
||||
@ -108,9 +105,9 @@ module 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
|
||||
check_overflow
|
||||
end
|
||||
|
||||
# Merge a hash of variables in the current local scope
|
||||
@ -131,20 +128,32 @@ module Liquid
|
||||
# context['var'] = 'hi'
|
||||
# end
|
||||
#
|
||||
# context['var] #=> nil
|
||||
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
|
||||
|
||||
# context['var'] #=> nil
|
||||
def stack(new_scope = {})
|
||||
push(new_scope)
|
||||
yield
|
||||
ensure
|
||||
pop if @this_stack_used
|
||||
@this_stack_used = old_stack_used
|
||||
pop
|
||||
end
|
||||
|
||||
# Creates a new context inheriting resource limits, filters, environment etc.,
|
||||
# but with an isolated scope.
|
||||
def new_isolated_subcontext
|
||||
check_overflow
|
||||
|
||||
self.class.build(
|
||||
resource_limits: resource_limits,
|
||||
static_environments: static_environments,
|
||||
registers: Registers.new(registers),
|
||||
).tap do |subcontext|
|
||||
subcontext.base_scope_depth = base_scope_depth + 1
|
||||
subcontext.exception_renderer = exception_renderer
|
||||
subcontext.filters = @filters
|
||||
subcontext.strainer = nil
|
||||
subcontext.errors = errors
|
||||
subcontext.warnings = warnings
|
||||
subcontext.disabled_tags = @disabled_tags
|
||||
end
|
||||
end
|
||||
|
||||
def clear_instance_assigns
|
||||
@ -153,10 +162,6 @@ 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
|
||||
|
||||
@ -172,7 +177,7 @@ module Liquid
|
||||
evaluate(Expression.parse(expression))
|
||||
end
|
||||
|
||||
def has_key?(key)
|
||||
def key?(key)
|
||||
self[key] != nil
|
||||
end
|
||||
|
||||
@ -181,52 +186,100 @@ module Liquid
|
||||
end
|
||||
|
||||
# Fetches an object starting at the local scope and then moving up the hierachy
|
||||
def find_variable(key)
|
||||
|
||||
def find_variable(key, raise_on_not_found: true)
|
||||
# This was changed from find() to find_index() because this is a very hot
|
||||
# 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
|
||||
index = @scopes.find_index { |s| s.key?(key) }
|
||||
|
||||
variable = nil
|
||||
|
||||
if scope.nil?
|
||||
@environments.each do |e|
|
||||
variable = lookup_and_evaluate(e, key)
|
||||
unless variable.nil?
|
||||
scope = e
|
||||
break
|
||||
end
|
||||
end
|
||||
variable = if index
|
||||
lookup_and_evaluate(@scopes[index], key, raise_on_not_found: raise_on_not_found)
|
||||
else
|
||||
try_variable_find_in_environments(key, raise_on_not_found: raise_on_not_found)
|
||||
end
|
||||
|
||||
scope ||= @environments.last || @scopes.last
|
||||
variable ||= lookup_and_evaluate(scope, key)
|
||||
|
||||
variable = variable.to_liquid
|
||||
variable = variable.to_liquid
|
||||
variable.context = self if variable.respond_to?(:context=)
|
||||
|
||||
return variable
|
||||
variable
|
||||
end
|
||||
|
||||
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)
|
||||
def lookup_and_evaluate(obj, key, raise_on_not_found: true)
|
||||
if @strict_variables && raise_on_not_found && obj.respond_to?(:key?) && !obj.key?(key)
|
||||
raise Liquid::UndefinedVariable, "undefined variable #{key}"
|
||||
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
|
||||
|
||||
def with_disabled_tags(tag_names)
|
||||
tag_names.each do |name|
|
||||
@disabled_tags[name] = @disabled_tags.fetch(name, 0) + 1
|
||||
end
|
||||
yield
|
||||
ensure
|
||||
tag_names.each do |name|
|
||||
@disabled_tags[name] -= 1
|
||||
end
|
||||
end
|
||||
|
||||
def tag_disabled?(tag_name)
|
||||
@disabled_tags.fetch(tag_name, 0) > 0
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_writer :base_scope_depth, :warnings, :errors, :strainer, :filters, :disabled_tags
|
||||
|
||||
private
|
||||
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
|
||||
|
||||
attr_reader :base_scope_depth
|
||||
|
||||
def try_variable_find_in_environments(key, raise_on_not_found:)
|
||||
@environments.each do |environment|
|
||||
found_variable = lookup_and_evaluate(environment, key, raise_on_not_found: raise_on_not_found)
|
||||
if !found_variable.nil? || @strict_variables && raise_on_not_found
|
||||
return found_variable
|
||||
end
|
||||
end
|
||||
@static_environments.each do |environment|
|
||||
found_variable = lookup_and_evaluate(environment, key, raise_on_not_found: raise_on_not_found)
|
||||
if !found_variable.nil? || @strict_variables && raise_on_not_found
|
||||
return found_variable
|
||||
end
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
def check_overflow
|
||||
raise StackLevelError, "Nesting too deep" if overflow?
|
||||
end
|
||||
|
||||
def overflow?
|
||||
base_scope_depth + @scopes.length > Block::MAX_DEPTH
|
||||
end
|
||||
|
||||
def internal_error
|
||||
# raise and catch to set backtrace and cause on exception
|
||||
raise Liquid::InternalError, 'internal'
|
||||
rescue Liquid::InternalError => exc
|
||||
exc
|
||||
end
|
||||
|
||||
def squash_instance_assigns_with_environments
|
||||
@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 # squash_instance_assigns_with_environments
|
||||
end
|
||||
end # squash_instance_assigns_with_environments
|
||||
end # Context
|
||||
end # Liquid
|
||||
|
@ -1,23 +1,64 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Document < BlockBody
|
||||
def self.parse(tokens, options)
|
||||
doc = new
|
||||
doc.parse(tokens, options)
|
||||
class Document
|
||||
def self.parse(tokens, parse_context)
|
||||
doc = new(parse_context)
|
||||
doc.parse(tokens, parse_context)
|
||||
doc
|
||||
end
|
||||
|
||||
def parse(tokens, options)
|
||||
super do |end_tag_name, end_tag_params|
|
||||
unknown_tag(end_tag_name, options) if end_tag_name
|
||||
attr_reader :parse_context, :body
|
||||
|
||||
def initialize(parse_context)
|
||||
@parse_context = parse_context
|
||||
@body = new_body
|
||||
end
|
||||
|
||||
def nodelist
|
||||
@body.nodelist
|
||||
end
|
||||
|
||||
def parse(tokenizer, parse_context)
|
||||
while parse_body(tokenizer)
|
||||
end
|
||||
@body.freeze
|
||||
rescue SyntaxError => e
|
||||
e.line_number ||= parse_context.line_number
|
||||
raise
|
||||
end
|
||||
|
||||
def unknown_tag(tag, _markup, _tokenizer)
|
||||
case tag
|
||||
when 'else', 'end'
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_outer_tag", tag: tag)
|
||||
else
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag)
|
||||
end
|
||||
end
|
||||
|
||||
def unknown_tag(tag, options)
|
||||
case tag
|
||||
when 'else'.freeze, 'end'.freeze
|
||||
raise SyntaxError.new(options[:locale].t("errors.syntax.unexpected_outer_tag".freeze, :tag => tag))
|
||||
else
|
||||
raise SyntaxError.new(options[:locale].t("errors.syntax.unknown_tag".freeze, :tag => tag))
|
||||
def render_to_output_buffer(context, output)
|
||||
@body.render_to_output_buffer(context, output)
|
||||
end
|
||||
|
||||
def render(context)
|
||||
render_to_output_buffer(context, +'')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def new_body
|
||||
parse_context.new_block_body
|
||||
end
|
||||
|
||||
def parse_body(tokenizer)
|
||||
@body.parse(tokenizer, parse_context) do |unknown_tag_name, unknown_tag_markup|
|
||||
if unknown_tag_name
|
||||
unknown_tag(unknown_tag_name, unknown_tag_markup, tokenizer)
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1,7 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
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 +20,31 @@ 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
|
||||
def initialize
|
||||
@context = nil
|
||||
end
|
||||
|
||||
# Catch all for the method
|
||||
def before_method(method)
|
||||
nil
|
||||
def liquid_method_missing(method)
|
||||
return nil unless @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 +60,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?]
|
||||
blacklist -= [:sort, :count, :first, :min, :max]
|
||||
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,10 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Error < ::StandardError
|
||||
attr_accessor :line_number
|
||||
attr_accessor :template_name
|
||||
attr_accessor :markup_context
|
||||
|
||||
def to_s(with_prefix=true)
|
||||
str = ""
|
||||
def to_s(with_prefix = true)
|
||||
str = +""
|
||||
str << message_prefix if with_prefix
|
||||
str << super()
|
||||
|
||||
@ -16,32 +19,20 @@ module Liquid
|
||||
str
|
||||
end
|
||||
|
||||
def set_line_number_from_token(token)
|
||||
return unless token.respond_to?(:line_number)
|
||||
return if self.line_number
|
||||
self.line_number = token.line_number
|
||||
end
|
||||
|
||||
def self.render(e)
|
||||
if e.is_a?(Liquid::Error)
|
||||
e.to_s
|
||||
else
|
||||
"Liquid error: #{e.to_s}"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def message_prefix
|
||||
str = ""
|
||||
if is_a?(SyntaxError)
|
||||
str << "Liquid syntax error"
|
||||
str = +""
|
||||
str << if is_a?(SyntaxError)
|
||||
"Liquid syntax error"
|
||||
else
|
||||
str << "Liquid error"
|
||||
"Liquid error"
|
||||
end
|
||||
|
||||
if line_number
|
||||
str << " (line #{line_number})"
|
||||
str << " ("
|
||||
str << template_name << " " if template_name
|
||||
str << "line " << line_number.to_s << ")"
|
||||
end
|
||||
|
||||
str << ": "
|
||||
@ -49,12 +40,19 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
class ArgumentError < Error; end
|
||||
class ContextError < Error; end
|
||||
class FileSystemError < Error; end
|
||||
class StandardError < Error; end
|
||||
class SyntaxError < Error; end
|
||||
class StackLevelError < Error; end
|
||||
class TaintedError < Error; end
|
||||
class MemoryError < Error; end
|
||||
ArgumentError = Class.new(Error)
|
||||
ContextError = Class.new(Error)
|
||||
FileSystemError = Class.new(Error)
|
||||
StandardError = Class.new(Error)
|
||||
SyntaxError = Class.new(Error)
|
||||
StackLevelError = Class.new(Error)
|
||||
MemoryError = Class.new(Error)
|
||||
ZeroDivisionError = Class.new(Error)
|
||||
FloatDomainError = Class.new(Error)
|
||||
UndefinedVariable = Class.new(Error)
|
||||
UndefinedDropMethod = Class.new(Error)
|
||||
UndefinedFilter = Class.new(Error)
|
||||
MethodOverrideError = Class.new(Error)
|
||||
DisabledError = Class.new(Error)
|
||||
InternalError = Class.new(Error)
|
||||
end
|
||||
|
@ -1,33 +1,48 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Expression
|
||||
LITERALS = {
|
||||
nil => nil, 'nil'.freeze => nil, 'null'.freeze => nil, ''.freeze => nil,
|
||||
'true'.freeze => true,
|
||||
'false'.freeze => false,
|
||||
'blank'.freeze => :blank?,
|
||||
'empty'.freeze => :empty?
|
||||
}
|
||||
nil => nil,
|
||||
'nil' => nil,
|
||||
'null' => nil,
|
||||
'' => nil,
|
||||
'true' => true,
|
||||
'false' => false,
|
||||
'blank' => '',
|
||||
'empty' => ''
|
||||
}.freeze
|
||||
|
||||
INTEGERS_REGEX = /\A(-?\d+)\z/
|
||||
FLOATS_REGEX = /\A(-?\d[\d\.]+)\z/
|
||||
|
||||
# Use an atomic group (?>...) to avoid pathological backtracing from
|
||||
# malicious input as described in https://github.com/Shopify/liquid/issues/1357
|
||||
RANGES_REGEX = /\A\(\s*(?>(\S+)\s*\.\.)\s*(\S+)\s*\)\z/
|
||||
|
||||
def self.parse(markup)
|
||||
if LITERALS.key?(markup)
|
||||
LITERALS[markup]
|
||||
return nil unless markup
|
||||
|
||||
markup = markup.strip
|
||||
if (markup.start_with?('"') && markup.end_with?('"')) ||
|
||||
(markup.start_with?("'") && markup.end_with?("'"))
|
||||
return markup[1..-2]
|
||||
end
|
||||
|
||||
case markup
|
||||
when INTEGERS_REGEX
|
||||
Regexp.last_match(1).to_i
|
||||
when RANGES_REGEX
|
||||
RangeLookup.parse(Regexp.last_match(1), Regexp.last_match(2))
|
||||
when FLOATS_REGEX
|
||||
Regexp.last_match(1).to_f
|
||||
else
|
||||
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
|
||||
if LITERALS.key?(markup)
|
||||
LITERALS[markup]
|
||||
else
|
||||
VariableLookup.parse(markup)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'time'
|
||||
require 'date'
|
||||
|
||||
@ -7,44 +9,56 @@ class String # :nodoc:
|
||||
end
|
||||
end
|
||||
|
||||
class Array # :nodoc:
|
||||
class Symbol # :nodoc:
|
||||
def to_liquid
|
||||
to_s
|
||||
end
|
||||
end
|
||||
|
||||
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 Range # :nodoc:
|
||||
def to_liquid
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
class DateTime < Date # :nodoc:
|
||||
class Time # :nodoc:
|
||||
def to_liquid
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
class Date # :nodoc:
|
||||
class DateTime < Date # :nodoc:
|
||||
def to_liquid
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
class Date # :nodoc:
|
||||
def to_liquid
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
class TrueClass
|
||||
def to_liquid # :nodoc:
|
||||
def to_liquid # :nodoc:
|
||||
self
|
||||
end
|
||||
end
|
||||
|
@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# A Liquid file system is a way to let your templates retrieve other templates for use with the include tag.
|
||||
#
|
||||
@ -8,13 +10,13 @@ module Liquid
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# Liquid::Template.file_system = Liquid::LocalFileSystem.new(template_path)
|
||||
# liquid = Liquid::Template.parse(template)
|
||||
# Liquid::Template.file_system = Liquid::LocalFileSystem.new(template_path)
|
||||
# liquid = Liquid::Template.parse(template)
|
||||
#
|
||||
# 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
|
||||
@ -26,10 +28,10 @@ module Liquid
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# file_system = Liquid::LocalFileSystem.new("/some/path")
|
||||
# file_system = Liquid::LocalFileSystem.new("/some/path")
|
||||
#
|
||||
# file_system.full_path("mypartial") # => "/some/path/_mypartial.liquid"
|
||||
# file_system.full_path("dir/mypartial") # => "/some/path/dir/_mypartial.liquid"
|
||||
# file_system.full_path("mypartial") # => "/some/path/_mypartial.liquid"
|
||||
# file_system.full_path("dir/mypartial") # => "/some/path/dir/_mypartial.liquid"
|
||||
#
|
||||
# Optionally in the second argument you can specify a custom pattern for template filenames.
|
||||
# The Kernel::sprintf format specification is used.
|
||||
@ -37,35 +39,35 @@ module Liquid
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# file_system = Liquid::LocalFileSystem.new("/some/path", "%s.html")
|
||||
# file_system = Liquid::LocalFileSystem.new("/some/path", "%s.html")
|
||||
#
|
||||
# file_system.full_path("index") # => "/some/path/index.html"
|
||||
# file_system.full_path("index") # => "/some/path/index.html"
|
||||
#
|
||||
class LocalFileSystem
|
||||
attr_accessor :root
|
||||
|
||||
def initialize(root, pattern = "_%s.liquid".freeze)
|
||||
@root = root
|
||||
def initialize(root, pattern = "_%s.liquid")
|
||||
@root = root
|
||||
@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
|
||||
|
||||
def full_path(template_path)
|
||||
raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ /\A[^.\/][a-zA-Z0-9_\/]+\z/
|
||||
raise FileSystemError, "Illegal template name '#{template_path}'" unless %r{\A[^./][a-zA-Z0-9_/]+\z}.match?(template_path)
|
||||
|
||||
full_path = if template_path.include?('/'.freeze)
|
||||
full_path = if template_path.include?('/')
|
||||
File.join(root, File.dirname(template_path), @pattern % File.basename(template_path))
|
||||
else
|
||||
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
|
||||
|
89
lib/liquid/forloop_drop.rb
Normal file
89
lib/liquid/forloop_drop.rb
Normal file
@ -0,0 +1,89 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# @liquid_public_docs
|
||||
# @liquid_type object
|
||||
# @liquid_name forloop
|
||||
# @liquid_summary
|
||||
# Information about a parent [`for` loop](/docs/api/liquid/tags/for).
|
||||
class ForloopDrop < Drop
|
||||
def initialize(name, length, parentloop)
|
||||
@name = name
|
||||
@length = length
|
||||
@parentloop = parentloop
|
||||
@index = 0
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_name length
|
||||
# @liquid_summary
|
||||
# The total number of iterations in the loop.
|
||||
# @liquid_return [number]
|
||||
attr_reader :length
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_name parentloop
|
||||
# @liquid_summary
|
||||
# The parent `forloop` object.
|
||||
# @liquid_description
|
||||
# If the current `for` loop isn't nested inside another `for` loop, then `nil` is returned.
|
||||
# @liquid_return [forloop]
|
||||
attr_reader :parentloop
|
||||
|
||||
attr_reader :name
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The 1-based index of the current iteration.
|
||||
# @liquid_return [number]
|
||||
def index
|
||||
@index + 1
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The 0-based index of the current iteration.
|
||||
# @liquid_return [number]
|
||||
def index0
|
||||
@index
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The 1-based index of the current iteration, in reverse order.
|
||||
# @liquid_return [number]
|
||||
def rindex
|
||||
@length - @index
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The 0-based index of the current iteration, in reverse order.
|
||||
# @liquid_return [number]
|
||||
def rindex0
|
||||
@length - @index - 1
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# Returns `true` if the current iteration is the first. Returns `false` if not.
|
||||
# @liquid_return [boolean]
|
||||
def first
|
||||
@index == 0
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# Returns `true` if the current iteration is the last. Returns `false` if not.
|
||||
# @liquid_return [boolean]
|
||||
def last
|
||||
@index == @length - 1
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def increment!
|
||||
@index += 1
|
||||
end
|
||||
end
|
||||
end
|
@ -1,11 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
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,16 +24,17 @@ 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]}"
|
||||
}
|
||||
(vars[Regexp.last_match(1).to_sym]).to_s
|
||||
end
|
||||
end
|
||||
|
||||
def deep_fetch_translation(name)
|
||||
name.split('.'.freeze).reduce(locale) do |level, cur|
|
||||
level[cur] or raise TranslationError, "Translation for #{name} does not exist in locale #{path}"
|
||||
name.split('.').reduce(locale) do |level, cur|
|
||||
level[cur] || raise(TranslationError, "Translation for #{name} does not exist in locale #{path}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1,11 +1,12 @@
|
||||
module Liquid
|
||||
# frozen_string_literal: true
|
||||
|
||||
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)
|
||||
@message = message || "interrupt".freeze
|
||||
def initialize(message = nil)
|
||||
@message = message || "interrupt"
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -1,45 +1,53 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "strscan"
|
||||
module Liquid
|
||||
class Lexer
|
||||
SPECIALS = {
|
||||
'|'.freeze => :pipe,
|
||||
'.'.freeze => :dot,
|
||||
':'.freeze => :colon,
|
||||
','.freeze => :comma,
|
||||
'['.freeze => :open_square,
|
||||
']'.freeze => :close_square,
|
||||
'('.freeze => :open_round,
|
||||
')'.freeze => :close_round,
|
||||
'?'.freeze => :question,
|
||||
'-'.freeze => :dash
|
||||
}
|
||||
IDENTIFIER = /[a-zA-Z_][\w-]*\??/
|
||||
'|' => :pipe,
|
||||
'.' => :dot,
|
||||
':' => :colon,
|
||||
',' => :comma,
|
||||
'[' => :open_square,
|
||||
']' => :close_square,
|
||||
'(' => :open_round,
|
||||
')' => :close_round,
|
||||
'?' => :question,
|
||||
'-' => :dash,
|
||||
}.freeze
|
||||
IDENTIFIER = /[a-zA-Z_][\w-]*\??/
|
||||
SINGLE_STRING_LITERAL = /'[^\']*'/
|
||||
DOUBLE_STRING_LITERAL = /"[^\"]*"/
|
||||
NUMBER_LITERAL = /-?\d+(\.\d+)?/
|
||||
DOTDOT = /\.\./
|
||||
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains/
|
||||
STRING_LITERAL = Regexp.union(SINGLE_STRING_LITERAL, DOUBLE_STRING_LITERAL)
|
||||
NUMBER_LITERAL = /-?\d+(\.\d+)?/
|
||||
DOTDOT = /\.\./
|
||||
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/
|
||||
WHITESPACE_OR_NOTHING = /\s*/
|
||||
|
||||
def initialize(input)
|
||||
@ss = StringScanner.new(input.rstrip)
|
||||
@ss = StringScanner.new(input)
|
||||
end
|
||||
|
||||
def tokenize
|
||||
@output = []
|
||||
|
||||
while !@ss.eos?
|
||||
@ss.skip(/\s*/)
|
||||
tok = case
|
||||
when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t]
|
||||
when t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t]
|
||||
when t = @ss.scan(DOUBLE_STRING_LITERAL) then [:string, t]
|
||||
when t = @ss.scan(NUMBER_LITERAL) then [:number, t]
|
||||
when t = @ss.scan(IDENTIFIER) then [:id, t]
|
||||
when t = @ss.scan(DOTDOT) then [:dotdot, t]
|
||||
until @ss.eos?
|
||||
@ss.skip(WHITESPACE_OR_NOTHING)
|
||||
break if @ss.eos?
|
||||
tok = if (t = @ss.scan(COMPARISON_OPERATOR))
|
||||
[:comparison, t]
|
||||
elsif (t = @ss.scan(STRING_LITERAL))
|
||||
[:string, t]
|
||||
elsif (t = @ss.scan(NUMBER_LITERAL))
|
||||
[:number, t]
|
||||
elsif (t = @ss.scan(IDENTIFIER))
|
||||
[:id, t]
|
||||
elsif (t = @ss.scan(DOTDOT))
|
||||
[:dotdot, t]
|
||||
else
|
||||
c = @ss.getch
|
||||
if s = SPECIALS[c]
|
||||
[s,c]
|
||||
c = @ss.getch
|
||||
if (s = SPECIALS[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]"
|
||||
@ -12,12 +13,17 @@
|
||||
for_invalid_attribute: "Invalid attribute in for loop. Valid attributes are limit and offset"
|
||||
if: "Syntax Error in tag 'if' - Valid syntax: if [expression]"
|
||||
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}"
|
||||
inline_comment_invalid: "Syntax error in tag '#' - Each line of comments must be prefixed by the '#' character"
|
||||
invalid_delimiter: "'%{tag}' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
|
||||
render: "Syntax error in tag 'render' - Template name must be a quoted string"
|
||||
table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
|
||||
tag_never_closed: "'%{block_name}' tag was never closed"
|
||||
tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
|
||||
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}"
|
||||
unknown_tag: "Unknown tag '%{tag}'"
|
||||
variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}"
|
||||
tag_never_closed: "'%{block_name}' tag was never closed"
|
||||
meta_syntax_error: "Liquid syntax error: #{e.message}"
|
||||
table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
|
||||
argument:
|
||||
include: "Argument error in tag 'include' - Illegal template name"
|
||||
disabled:
|
||||
tag: "usage is not allowed in this context"
|
||||
|
@ -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
|
54
lib/liquid/parse_context.rb
Normal file
54
lib/liquid/parse_context.rb
Normal file
@ -0,0 +1,54 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class ParseContext
|
||||
attr_accessor :locale, :line_number, :trim_whitespace, :depth
|
||||
attr_reader :partial, :warnings, :error_mode
|
||||
|
||||
def initialize(options = {})
|
||||
@template_options = options ? options.dup : {}
|
||||
|
||||
@locale = @template_options[:locale] ||= I18n.new
|
||||
@warnings = []
|
||||
|
||||
self.depth = 0
|
||||
self.partial = false
|
||||
end
|
||||
|
||||
def [](option_key)
|
||||
@options[option_key]
|
||||
end
|
||||
|
||||
def new_block_body
|
||||
Liquid::BlockBody.new
|
||||
end
|
||||
|
||||
def new_tokenizer(markup, start_line_number: nil, for_liquid_tag: false)
|
||||
Tokenizer.new(markup, line_number: start_line_number, for_liquid_tag: for_liquid_tag)
|
||||
end
|
||||
|
||||
def parse_expression(markup)
|
||||
Expression.parse(markup)
|
||||
end
|
||||
|
||||
def partial=(value)
|
||||
@partial = value
|
||||
@options = value ? partial_options : @template_options
|
||||
|
||||
@error_mode = @options[:error_mode] || Template.error_mode
|
||||
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
|
42
lib/liquid/parse_tree_visitor.rb
Normal file
42
lib/liquid/parse_tree_visitor.rb
Normal file
@ -0,0 +1,42 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class ParseTreeVisitor
|
||||
def self.for(node, callbacks = Hash.new(proc {}))
|
||||
if defined?(node.class::ParseTreeVisitor)
|
||||
node.class::ParseTreeVisitor
|
||||
else
|
||||
self
|
||||
end.new(node, callbacks)
|
||||
end
|
||||
|
||||
def initialize(node, callbacks)
|
||||
@node = node
|
||||
@callbacks = callbacks
|
||||
end
|
||||
|
||||
def add_callback_for(*classes, &block)
|
||||
callback = block
|
||||
callback = ->(node, _) { yield node } if block.arity.abs == 1
|
||||
callback = ->(_, _) { yield } if block.arity.zero?
|
||||
classes.each { |klass| @callbacks[klass] = callback }
|
||||
self
|
||||
end
|
||||
|
||||
def visit(context = nil)
|
||||
children.map do |node|
|
||||
item, new_context = @callbacks[node.class].call(node, context)
|
||||
[
|
||||
item,
|
||||
ParseTreeVisitor.for(node, @callbacks).visit(new_context || context),
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def children
|
||||
@node.respond_to?(:nodelist) ? Array(@node.nodelist) : []
|
||||
end
|
||||
end
|
||||
end
|
@ -1,9 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Parser
|
||||
def initialize(input)
|
||||
l = Lexer.new(input)
|
||||
l = Lexer.new(input)
|
||||
@tokens = l.tokenize
|
||||
@p = 0 # pointer to current location
|
||||
@p = 0 # pointer to current location
|
||||
end
|
||||
|
||||
def jump(point)
|
||||
@ -46,11 +48,18 @@ module Liquid
|
||||
|
||||
def expression
|
||||
token = @tokens[@p]
|
||||
if token[0] == :id
|
||||
variable_signature
|
||||
elsif [:string, :number].include? token[0]
|
||||
case token[0]
|
||||
when :id
|
||||
str = consume
|
||||
str << variable_lookups
|
||||
when :open_square
|
||||
str = consume
|
||||
str << expression
|
||||
str << consume(:close_square)
|
||||
str << variable_lookups
|
||||
when :string, :number
|
||||
consume
|
||||
elsif token.first == :open_round
|
||||
when :open_round
|
||||
consume
|
||||
first = expression
|
||||
consume(:dotdot)
|
||||
@ -63,26 +72,29 @@ module Liquid
|
||||
end
|
||||
|
||||
def argument
|
||||
str = ""
|
||||
str = +""
|
||||
# might be a keyword argument (identifier: expression)
|
||||
if look(:id) && look(:colon, 1)
|
||||
str << consume << consume << ' '.freeze
|
||||
str << consume << consume << ' '
|
||||
end
|
||||
|
||||
str << expression
|
||||
str
|
||||
end
|
||||
|
||||
def variable_signature
|
||||
str = consume(:id)
|
||||
if look(:open_square)
|
||||
str << consume
|
||||
str << expression
|
||||
str << consume(:close_square)
|
||||
end
|
||||
if look(:dot)
|
||||
str << consume
|
||||
str << variable_signature
|
||||
def variable_lookups
|
||||
str = +""
|
||||
loop do
|
||||
if look(:open_square)
|
||||
str << consume
|
||||
str << expression
|
||||
str << consume(:close_square)
|
||||
elsif look(:dot)
|
||||
str << consume
|
||||
str << consume(:id)
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
str
|
||||
end
|
||||
|
@ -1,25 +1,39 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
module ParserSwitching
|
||||
def strict_parse_with_error_mode_fallback(markup)
|
||||
strict_parse_with_error_context(markup)
|
||||
rescue SyntaxError => e
|
||||
case parse_context.error_mode
|
||||
when :strict
|
||||
raise
|
||||
when :warn
|
||||
parse_context.warnings << e
|
||||
end
|
||||
lax_parse(markup)
|
||||
end
|
||||
|
||||
def parse_with_selected_parser(markup)
|
||||
case @options[:error_mode] || Template.error_mode
|
||||
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)
|
||||
strict_parse_with_error_context(markup)
|
||||
rescue SyntaxError => e
|
||||
e.set_line_number_from_token(markup)
|
||||
@warnings ||= []
|
||||
@warnings << e
|
||||
return lax_parse(markup)
|
||||
parse_context.warnings << e
|
||||
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
|
||||
|
33
lib/liquid/partial_cache.rb
Normal file
33
lib/liquid/partial_cache.rb
Normal file
@ -0,0 +1,33 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class PartialCache
|
||||
def self.load(template_name, context:, parse_context:)
|
||||
cached_partials = context.registers[:cached_partials]
|
||||
cache_key = "#{template_name}:#{parse_context.error_mode}"
|
||||
cached = cached_partials[cache_key]
|
||||
return cached if cached
|
||||
|
||||
file_system = context.registers[:file_system]
|
||||
source = file_system.read_template_file(template_name)
|
||||
|
||||
parse_context.partial = true
|
||||
|
||||
template_factory = context.registers[:template_factory]
|
||||
template = template_factory.for(template_name)
|
||||
|
||||
begin
|
||||
partial = template.parse(source, parse_context)
|
||||
rescue Liquid::Error => e
|
||||
e.template_name = template&.name || template_name
|
||||
raise e
|
||||
end
|
||||
|
||||
partial.name ||= template_name
|
||||
|
||||
cached_partials[cache_key] = partial
|
||||
ensure
|
||||
parse_context.partial = false
|
||||
end
|
||||
end
|
||||
end
|
@ -1,9 +1,13 @@
|
||||
module Liquid
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'liquid/profiler/hooks'
|
||||
|
||||
module Liquid
|
||||
# Profiler enables support for profiling template rendering to help track down performance issues.
|
||||
#
|
||||
# To enable profiling, pass the <tt>profile: true</tt> option to <tt>Liquid::Template.parse</tt>. Then, after
|
||||
# <tt>Liquid::Template#render</tt> is called, the template object makes available an instance of this
|
||||
# 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)
|
||||
@ -17,11 +21,11 @@ module Liquid
|
||||
# inside of <tt>{% include %}</tt> tags.
|
||||
#
|
||||
# profile.each do |node|
|
||||
# # Access to the token itself
|
||||
# # Access to the node itself
|
||||
# node.code
|
||||
#
|
||||
# # Which template and line number of this node.
|
||||
# # If top level, this will be "<root>".
|
||||
# # The top-level template name is `nil` by default, but can be set in the Liquid::Context before rendering.
|
||||
# node.partial
|
||||
# node.line_number
|
||||
#
|
||||
@ -42,118 +46,94 @@ module Liquid
|
||||
include Enumerable
|
||||
|
||||
class Timing
|
||||
attr_reader :code, :partial, :line_number, :children
|
||||
attr_reader :code, :template_name, :line_number, :children
|
||||
attr_accessor :total_time
|
||||
alias_method :render_time, :total_time
|
||||
alias_method :partial, :template_name
|
||||
|
||||
def initialize(token, partial)
|
||||
@code = token.respond_to?(:raw) ? token.raw : token
|
||||
@partial = partial
|
||||
@line_number = token.respond_to?(:line_number) ? token.line_number : nil
|
||||
@children = []
|
||||
def initialize(code: nil, template_name: nil, line_number: nil)
|
||||
@code = code
|
||||
@template_name = template_name
|
||||
@line_number = line_number
|
||||
@children = []
|
||||
end
|
||||
|
||||
def self.start(token, partial)
|
||||
new(token, partial).tap do |t|
|
||||
t.start
|
||||
def self_time
|
||||
@self_time ||= begin
|
||||
total_children_time = 0.0
|
||||
@children.each do |child|
|
||||
total_children_time += child.total_time
|
||||
end
|
||||
@total_time - total_children_time
|
||||
end
|
||||
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_token_render(token)
|
||||
if Profiler.current_profile && token.respond_to?(:render)
|
||||
Profiler.current_profile.start_token(token)
|
||||
output = yield
|
||||
Profiler.current_profile.end_token(token)
|
||||
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
|
||||
attr_reader :total_time
|
||||
alias_method :total_render_time, :total_time
|
||||
|
||||
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
|
||||
@root_children = []
|
||||
@current_children = nil
|
||||
@total_time = 0.0
|
||||
end
|
||||
|
||||
def start
|
||||
Thread.current[:liquid_profiler] = self
|
||||
@render_start_at = Time.now
|
||||
def profile(template_name, &block)
|
||||
# nested renders are done from a tag that already has a timing node
|
||||
return yield if @current_children
|
||||
|
||||
root_children = @root_children
|
||||
render_idx = root_children.length
|
||||
begin
|
||||
@current_children = root_children
|
||||
profile_node(template_name, &block)
|
||||
ensure
|
||||
@current_children = nil
|
||||
if (timing = root_children[render_idx])
|
||||
@total_time += timing.total_time
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def stop
|
||||
Thread.current[:liquid_profiler] = nil
|
||||
@render_end_at = Time.now
|
||||
end
|
||||
|
||||
def total_render_time
|
||||
@render_end_at - @render_start_at
|
||||
def children
|
||||
children = @root_children
|
||||
if children.length == 1
|
||||
children.first.children
|
||||
else
|
||||
children
|
||||
end
|
||||
end
|
||||
|
||||
def each(&block)
|
||||
@root_timing.children.each(&block)
|
||||
children.each(&block)
|
||||
end
|
||||
|
||||
def [](idx)
|
||||
@root_timing.children[idx]
|
||||
children[idx]
|
||||
end
|
||||
|
||||
def length
|
||||
@root_timing.children.length
|
||||
children.length
|
||||
end
|
||||
|
||||
def start_token(token)
|
||||
@timing_stack.push(Timing.start(token, current_partial))
|
||||
def profile_node(template_name, code: nil, line_number: nil)
|
||||
timing = Timing.new(code: code, template_name: template_name, line_number: line_number)
|
||||
parent_children = @current_children
|
||||
start_time = monotonic_time
|
||||
begin
|
||||
@current_children = timing.children
|
||||
yield
|
||||
ensure
|
||||
@current_children = parent_children
|
||||
timing.total_time = monotonic_time - start_time
|
||||
parent_children << timing
|
||||
end
|
||||
end
|
||||
|
||||
def end_token(token)
|
||||
timing = @timing_stack.pop
|
||||
timing.finish
|
||||
private
|
||||
|
||||
@timing_stack.last.children << timing
|
||||
def monotonic_time
|
||||
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
||||
end
|
||||
|
||||
def current_partial
|
||||
@partial_stack.last
|
||||
end
|
||||
|
||||
def push_partial(partial_name)
|
||||
@partial_stack.push(partial_name)
|
||||
end
|
||||
|
||||
def pop_partial
|
||||
@partial_stack.pop
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
@ -1,23 +1,35 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class BlockBody
|
||||
def render_token_with_profiling(token, context)
|
||||
Profiler.profile_token_render(token) do
|
||||
render_token_without_profiling(token, context)
|
||||
module BlockBodyProfilingHook
|
||||
def render_node(context, output, node)
|
||||
if (profiler = context.profiler)
|
||||
profiler.profile_node(context.template_name, code: node.raw, line_number: node.line_number) do
|
||||
super
|
||||
end
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
alias_method :render_token_without_profiling, :render_token
|
||||
alias_method :render_token, :render_token_with_profiling
|
||||
end
|
||||
BlockBody.prepend(BlockBodyProfilingHook)
|
||||
|
||||
class Include < Tag
|
||||
def render_with_profiling(context)
|
||||
Profiler.profile_children(context.evaluate(@template_name).to_s) do
|
||||
render_without_profiling(context)
|
||||
end
|
||||
module DocumentProfilingHook
|
||||
def render_to_output_buffer(context, output)
|
||||
return super unless context.profiler
|
||||
context.profiler.profile(context.template_name) { super }
|
||||
end
|
||||
|
||||
alias_method :render_without_profiling, :render
|
||||
alias_method :render, :render_with_profiling
|
||||
end
|
||||
Document.prepend(DocumentProfilingHook)
|
||||
|
||||
module ContextProfilingHook
|
||||
attr_accessor :profiler
|
||||
|
||||
def new_isolated_subcontext
|
||||
new_context = super
|
||||
new_context.profiler = profiler
|
||||
new_context
|
||||
end
|
||||
end
|
||||
Context.prepend(ContextProfilingHook)
|
||||
end
|
||||
|
@ -1,22 +1,57 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class RangeLookup
|
||||
def self.parse(start_markup, end_markup)
|
||||
start_obj = Expression.parse(start_markup)
|
||||
end_obj = Expression.parse(end_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
|
||||
begin
|
||||
start_obj.to_i..end_obj.to_i
|
||||
rescue NoMethodError
|
||||
invalid_expr = start_markup unless start_obj.respond_to?(:to_i)
|
||||
invalid_expr ||= end_markup unless end_obj.respond_to?(:to_i)
|
||||
if invalid_expr
|
||||
raise Liquid::SyntaxError, "Invalid expression type '#{invalid_expr}' in range expression"
|
||||
end
|
||||
|
||||
raise
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :start_obj, :end_obj
|
||||
|
||||
def initialize(start_obj, end_obj)
|
||||
@start_obj = start_obj
|
||||
@end_obj = end_obj
|
||||
@end_obj = end_obj
|
||||
end
|
||||
|
||||
def evaluate(context)
|
||||
context.evaluate(@start_obj).to_i..context.evaluate(@end_obj).to_i
|
||||
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
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
[@node.start_obj, @node.end_obj]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
51
lib/liquid/registers.rb
Normal file
51
lib/liquid/registers.rb
Normal file
@ -0,0 +1,51 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Registers
|
||||
attr_reader :static
|
||||
|
||||
def initialize(registers = {})
|
||||
@static = registers.is_a?(Registers) ? registers.static : registers
|
||||
@changes = {}
|
||||
end
|
||||
|
||||
def []=(key, value)
|
||||
@changes[key] = value
|
||||
end
|
||||
|
||||
def [](key)
|
||||
if @changes.key?(key)
|
||||
@changes[key]
|
||||
else
|
||||
@static[key]
|
||||
end
|
||||
end
|
||||
|
||||
def delete(key)
|
||||
@changes.delete(key)
|
||||
end
|
||||
|
||||
UNDEFINED = Object.new
|
||||
|
||||
def fetch(key, default = UNDEFINED, &block)
|
||||
if @changes.key?(key)
|
||||
@changes.fetch(key)
|
||||
elsif default != UNDEFINED
|
||||
if block_given?
|
||||
@static.fetch(key, &block)
|
||||
else
|
||||
@static.fetch(key, default)
|
||||
end
|
||||
else
|
||||
@static.fetch(key, &block)
|
||||
end
|
||||
end
|
||||
|
||||
def key?(key)
|
||||
@changes.key?(key) || @static.key?(key)
|
||||
end
|
||||
end
|
||||
|
||||
# Alias for backwards compatibility
|
||||
StaticRegisters = Registers
|
||||
end
|
62
lib/liquid/resource_limits.rb
Normal file
62
lib/liquid/resource_limits.rb
Normal file
@ -0,0 +1,62 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class ResourceLimits
|
||||
attr_accessor :render_length_limit, :render_score_limit, :assign_score_limit
|
||||
attr_reader :render_score, :assign_score
|
||||
|
||||
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 increment_render_score(amount)
|
||||
@render_score += amount
|
||||
raise_limits_reached if @render_score_limit && @render_score > @render_score_limit
|
||||
end
|
||||
|
||||
def increment_assign_score(amount)
|
||||
@assign_score += amount
|
||||
raise_limits_reached if @assign_score_limit && @assign_score > @assign_score_limit
|
||||
end
|
||||
|
||||
# update either render_length or assign_score based on whether or not the writes are captured
|
||||
def increment_write_score(output)
|
||||
if (last_captured = @last_capture_length)
|
||||
captured = output.bytesize
|
||||
increment = captured - last_captured
|
||||
@last_capture_length = captured
|
||||
increment_assign_score(increment)
|
||||
elsif @render_length_limit && output.bytesize > @render_length_limit
|
||||
raise_limits_reached
|
||||
end
|
||||
end
|
||||
|
||||
def raise_limits_reached
|
||||
@reached_limit = true
|
||||
raise MemoryError, "Memory limits exceeded"
|
||||
end
|
||||
|
||||
def reached?
|
||||
@reached_limit
|
||||
end
|
||||
|
||||
def reset
|
||||
@reached_limit = false
|
||||
@last_capture_length = nil
|
||||
@render_score = @assign_score = 0
|
||||
end
|
||||
|
||||
def with_capture
|
||||
old_capture_length = @last_capture_length
|
||||
begin
|
||||
@last_capture_length = 0
|
||||
yield
|
||||
ensure
|
||||
@last_capture_length = old_capture_length
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
File diff suppressed because it is too large
Load Diff
@ -1,63 +0,0 @@
|
||||
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
|
||||
@@strainer_class_cache = Hash.new do |hash, filters|
|
||||
hash[filters] = Class.new(Strainer) do
|
||||
filters.each { |f| include f }
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(context)
|
||||
@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)
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
||||
def self.strainer_class_cache
|
||||
@@strainer_class_cache
|
||||
end
|
||||
|
||||
def self.create(context, filters = [])
|
||||
filters = @@filters + filters
|
||||
strainer_class_cache[filters].new(context)
|
||||
end
|
||||
|
||||
def invoke(method, *args)
|
||||
if invokable?(method)
|
||||
send(method, *args)
|
||||
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)
|
||||
end
|
||||
end
|
||||
end
|
41
lib/liquid/strainer_factory.rb
Normal file
41
lib/liquid/strainer_factory.rb
Normal file
@ -0,0 +1,41 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# StrainerFactory is the factory for the filters system.
|
||||
module StrainerFactory
|
||||
extend self
|
||||
|
||||
def add_global_filter(filter)
|
||||
strainer_class_cache.clear
|
||||
GlobalCache.add_filter(filter)
|
||||
end
|
||||
|
||||
def create(context, filters = [])
|
||||
strainer_from_cache(filters).new(context)
|
||||
end
|
||||
|
||||
def global_filter_names
|
||||
GlobalCache.filter_method_names
|
||||
end
|
||||
|
||||
GlobalCache = Class.new(StrainerTemplate)
|
||||
|
||||
private
|
||||
|
||||
def strainer_from_cache(filters)
|
||||
if filters.empty?
|
||||
GlobalCache
|
||||
else
|
||||
strainer_class_cache[filters] ||= begin
|
||||
klass = Class.new(GlobalCache)
|
||||
filters.each { |f| klass.add_filter(f) }
|
||||
klass
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def strainer_class_cache
|
||||
@strainer_class_cache ||= {}
|
||||
end
|
||||
end
|
||||
end
|
62
lib/liquid/strainer_template.rb
Normal file
62
lib/liquid/strainer_template.rb
Normal file
@ -0,0 +1,62 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'set'
|
||||
|
||||
module Liquid
|
||||
# StrainerTemplate is the computed class for the filters system.
|
||||
# New filters are mixed into the strainer class which is then instantiated for each liquid template render run.
|
||||
#
|
||||
# The Strainer only allows method calls defined in filters given to it via StrainerFactory.add_global_filter,
|
||||
# Context#add_filters or Template.register_filter
|
||||
class StrainerTemplate
|
||||
def initialize(context)
|
||||
@context = context
|
||||
end
|
||||
|
||||
class << self
|
||||
def add_filter(filter)
|
||||
return if include?(filter)
|
||||
|
||||
invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }
|
||||
if invokable_non_public_methods.any?
|
||||
raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"
|
||||
end
|
||||
|
||||
include(filter)
|
||||
|
||||
filter_methods.merge(filter.public_instance_methods.map(&:to_s))
|
||||
end
|
||||
|
||||
def invokable?(method)
|
||||
filter_methods.include?(method.to_s)
|
||||
end
|
||||
|
||||
def inherited(subclass)
|
||||
super
|
||||
subclass.instance_variable_set(:@filter_methods, @filter_methods.dup)
|
||||
end
|
||||
|
||||
def filter_method_names
|
||||
filter_methods.map(&:to_s).to_a
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def filter_methods
|
||||
@filter_methods ||= Set.new
|
||||
end
|
||||
end
|
||||
|
||||
def invoke(method, *args)
|
||||
if self.class.invokable?(method)
|
||||
send(method, *args)
|
||||
elsif @context.strict_filters
|
||||
raise Liquid::UndefinedFilter, "undefined filter #{method}"
|
||||
else
|
||||
args.first
|
||||
end
|
||||
rescue ::ArgumentError => e
|
||||
raise Liquid::ArgumentError, e.message, e.backtrace
|
||||
end
|
||||
end
|
||||
end
|
121
lib/liquid/tablerowloop_drop.rb
Normal file
121
lib/liquid/tablerowloop_drop.rb
Normal file
@ -0,0 +1,121 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# @liquid_public_docs
|
||||
# @liquid_type object
|
||||
# @liquid_name tablerowloop
|
||||
# @liquid_summary
|
||||
# Information about a parent [`tablerow` loop](/docs/api/liquid/tags/tablerow).
|
||||
class TablerowloopDrop < Drop
|
||||
def initialize(length, cols)
|
||||
@length = length
|
||||
@row = 1
|
||||
@col = 1
|
||||
@cols = cols
|
||||
@index = 0
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The total number of iterations in the loop.
|
||||
# @liquid_return [number]
|
||||
attr_reader :length
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The 1-based index of the current column.
|
||||
# @liquid_return [number]
|
||||
attr_reader :col
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The 1-based index of current row.
|
||||
# @liquid_return [number]
|
||||
attr_reader :row
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The 1-based index of the current iteration.
|
||||
# @liquid_return [number]
|
||||
def index
|
||||
@index + 1
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The 0-based index of the current iteration.
|
||||
# @liquid_return [number]
|
||||
def index0
|
||||
@index
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The 0-based index of the current column.
|
||||
# @liquid_return [number]
|
||||
def col0
|
||||
@col - 1
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The 1-based index of the current iteration, in reverse order.
|
||||
# @liquid_return [number]
|
||||
def rindex
|
||||
@length - @index
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The 0-based index of the current iteration, in reverse order.
|
||||
# @liquid_return [number]
|
||||
def rindex0
|
||||
@length - @index - 1
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# Returns `true` if the current iteration is the first. Returns `false` if not.
|
||||
# @liquid_return [boolean]
|
||||
def first
|
||||
@index == 0
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# Returns `true` if the current iteration is the last. Returns `false` if not.
|
||||
# @liquid_return [boolean]
|
||||
def last
|
||||
@index == @length - 1
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# Returns `true` if the current column is the first in the row. Returns `false` if not.
|
||||
# @liquid_return [boolean]
|
||||
def col_first
|
||||
@col == 1
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# Returns `true` if the current column is the last in the row. Returns `false` if not.
|
||||
# @liquid_return [boolean]
|
||||
def col_last
|
||||
@col == @cols
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def increment!
|
||||
@index += 1
|
||||
|
||||
if @col == @cols
|
||||
@col = 1
|
||||
@row += 1
|
||||
else
|
||||
@col += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,26 +1,41 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Tag
|
||||
attr_accessor :options, :line_number
|
||||
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)
|
||||
tag = new(tag_name, markup, options)
|
||||
tag.parse(tokens)
|
||||
def parse(tag_name, markup, tokenizer, parse_context)
|
||||
tag = new(tag_name, markup, parse_context)
|
||||
tag.parse(tokenizer)
|
||||
tag
|
||||
end
|
||||
|
||||
def disable_tags(*tag_names)
|
||||
tag_names += disabled_tags
|
||||
define_singleton_method(:disabled_tags) { tag_names }
|
||||
prepend(Disabler)
|
||||
end
|
||||
|
||||
private :new
|
||||
|
||||
protected
|
||||
|
||||
def disabled_tags
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
@tag_name = tag_name
|
||||
@markup = markup
|
||||
@options = options
|
||||
def initialize(tag_name, markup, parse_context)
|
||||
@tag_name = tag_name
|
||||
@markup = markup
|
||||
@parse_context = parse_context
|
||||
@line_number = parse_context.line_number
|
||||
end
|
||||
|
||||
def parse(tokens)
|
||||
def parse(_tokens)
|
||||
end
|
||||
|
||||
def raw
|
||||
@ -31,12 +46,26 @@ module Liquid
|
||||
self.class.name.downcase
|
||||
end
|
||||
|
||||
def render(context)
|
||||
''.freeze
|
||||
def render(_context)
|
||||
''
|
||||
end
|
||||
|
||||
# For backwards compatibility with custom tags. In a future release, the semantics
|
||||
# of the `render_to_output_buffer` method will become the default and the `render`
|
||||
# method will be removed.
|
||||
def render_to_output_buffer(context, output)
|
||||
output << render(context)
|
||||
output
|
||||
end
|
||||
|
||||
def blank?
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_expression(markup)
|
||||
parse_context.parse_expression(markup)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
22
lib/liquid/tag/disableable.rb
Normal file
22
lib/liquid/tag/disableable.rb
Normal file
@ -0,0 +1,22 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Tag
|
||||
module Disableable
|
||||
def render_to_output_buffer(context, output)
|
||||
if context.tag_disabled?(tag_name)
|
||||
output << disabled_error(context)
|
||||
return
|
||||
end
|
||||
super
|
||||
end
|
||||
|
||||
def disabled_error(context)
|
||||
# raise then rescue the exception so that the Context#exception_renderer can re-raise it
|
||||
raise DisabledError, "#{tag_name} #{parse_context[:locale].t('errors.disabled.tag')}"
|
||||
rescue DisabledError => exc
|
||||
context.handle_error(exc, line_number)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
13
lib/liquid/tag/disabler.rb
Normal file
13
lib/liquid/tag/disabler.rb
Normal file
@ -0,0 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Tag
|
||||
module Disabler
|
||||
def render_to_output_buffer(context, output)
|
||||
context.with_disabled_tags(self.class.disabled_tags) do
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,38 +1,77 @@
|
||||
module Liquid
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Assign sets a variable in your template.
|
||||
#
|
||||
# {% assign foo = 'monkey' %}
|
||||
#
|
||||
# You can then use the variable later in the page.
|
||||
#
|
||||
# {{ foo }}
|
||||
#
|
||||
module Liquid
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category variable
|
||||
# @liquid_name assign
|
||||
# @liquid_summary
|
||||
# Creates a new variable.
|
||||
# @liquid_description
|
||||
# You can create variables of any [basic type](/docs/api/liquid/basics#types), [object](/docs/api/liquid/objects), or object property.
|
||||
# @liquid_syntax
|
||||
# {% assign variable_name = value %}
|
||||
# @liquid_syntax_keyword variable_name The name of the variable being created.
|
||||
# @liquid_syntax_keyword value The value you want to assign to the variable.
|
||||
class Assign < Tag
|
||||
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
# @api private
|
||||
def self.raise_syntax_error(parse_context)
|
||||
raise Liquid::SyntaxError, parse_context.locale.t('errors.syntax.assign')
|
||||
end
|
||||
|
||||
attr_reader :to, :from
|
||||
|
||||
def initialize(tag_name, markup, parse_context)
|
||||
super
|
||||
if markup =~ Syntax
|
||||
@to = $1
|
||||
@from = Variable.new($2,options)
|
||||
@from.line_number = line_number
|
||||
@to = Regexp.last_match(1)
|
||||
@from = Variable.new(Regexp.last_match(2), parse_context)
|
||||
else
|
||||
raise SyntaxError.new options[:locale].t("errors.syntax.assign".freeze)
|
||||
self.class.raise_syntax_error(parse_context)
|
||||
end
|
||||
end
|
||||
|
||||
def render(context)
|
||||
def render_to_output_buffer(context, output)
|
||||
val = @from.render(context)
|
||||
context.scopes.last[@to] = val
|
||||
context.increment_used_resources(:assign_score_current, val)
|
||||
''.freeze
|
||||
context.resource_limits.increment_assign_score(assign_score_of(val))
|
||||
output
|
||||
end
|
||||
|
||||
def blank?
|
||||
true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def assign_score_of(val)
|
||||
if val.instance_of?(String)
|
||||
val.bytesize
|
||||
elsif val.instance_of?(Array)
|
||||
sum = 1
|
||||
# Uses #each to avoid extra allocations.
|
||||
val.each { |child| sum += assign_score_of(child) }
|
||||
sum
|
||||
elsif val.instance_of?(Hash)
|
||||
sum = 1
|
||||
val.each do |key, entry_value|
|
||||
sum += assign_score_of(key)
|
||||
sum += assign_score_of(entry_value)
|
||||
end
|
||||
sum
|
||||
else
|
||||
1
|
||||
end
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
[@node.from]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('assign'.freeze, Assign)
|
||||
Template.register_tag('assign', Assign)
|
||||
end
|
||||
|
@ -1,5 +1,6 @@
|
||||
module Liquid
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# Break tag to be used to break out of a for loop.
|
||||
#
|
||||
# == Basic Usage:
|
||||
@ -9,13 +10,22 @@ module Liquid
|
||||
# {% endif %}
|
||||
# {% endfor %}
|
||||
#
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category iteration
|
||||
# @liquid_name break
|
||||
# @liquid_summary
|
||||
# Stops a [`for` loop](/docs/api/liquid/tags/for) from iterating.
|
||||
# @liquid_syntax
|
||||
# {% break %}
|
||||
class Break < Tag
|
||||
INTERRUPT = BreakInterrupt.new.freeze
|
||||
|
||||
def interrupt
|
||||
BreakInterrupt.new
|
||||
def render_to_output_buffer(context, output)
|
||||
context.push_interrupt(INTERRUPT)
|
||||
output
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
Template.register_tag('break'.freeze, Break)
|
||||
Template.register_tag('break', Break)
|
||||
end
|
||||
|
@ -1,32 +1,38 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# Capture stores the result of a block into a variable without rendering it inplace.
|
||||
#
|
||||
# {% capture heading %}
|
||||
# Monkeys!
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category variable
|
||||
# @liquid_name capture
|
||||
# @liquid_summary
|
||||
# Creates a new variable with a string value.
|
||||
# @liquid_description
|
||||
# You can create complex strings with Liquid logic and variables.
|
||||
# @liquid_syntax
|
||||
# {% capture variable %}
|
||||
# value
|
||||
# {% endcapture %}
|
||||
# ...
|
||||
# <h1>{{ heading }}</h1>
|
||||
#
|
||||
# Capture is useful for saving content for use later in your template, such as
|
||||
# in a sidebar or footer.
|
||||
#
|
||||
# @liquid_syntax_keyword variable The name of the variable being created.
|
||||
# @liquid_syntax_keyword value The value you want to assign to the variable.
|
||||
class Capture < Block
|
||||
Syntax = /(\w+)/
|
||||
Syntax = /(#{VariableSignature}+)/o
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
if markup =~ Syntax
|
||||
@to = $1
|
||||
@to = Regexp.last_match(1)
|
||||
else
|
||||
raise SyntaxError.new(options[:locale].t("errors.syntax.capture"))
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.capture")
|
||||
end
|
||||
end
|
||||
|
||||
def render(context)
|
||||
output = super
|
||||
context.scopes.last[@to] = output
|
||||
context.increment_used_resources(:assign_score_current, output)
|
||||
''.freeze
|
||||
def render_to_output_buffer(context, output)
|
||||
context.resource_limits.with_capture do
|
||||
capture_output = render(context)
|
||||
context.scopes.last[@to] = capture_output
|
||||
end
|
||||
output
|
||||
end
|
||||
|
||||
def blank?
|
||||
@ -34,5 +40,5 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('capture'.freeze, Capture)
|
||||
Template.register_tag('capture', Capture)
|
||||
end
|
||||
|
@ -1,24 +1,55 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category conditional
|
||||
# @liquid_name case
|
||||
# @liquid_summary
|
||||
# Renders a specific expression depending on the value of a specific variable.
|
||||
# @liquid_syntax
|
||||
# {% case variable %}
|
||||
# {% when first_value %}
|
||||
# first_expression
|
||||
# {% when second_value %}
|
||||
# second_expression
|
||||
# {% else %}
|
||||
# third_expression
|
||||
# {% endcase %}
|
||||
# @liquid_syntax_keyword variable The name of the variable you want to base your case statement on.
|
||||
# @liquid_syntax_keyword first_value A specific value to check for.
|
||||
# @liquid_syntax_keyword second_value A specific value to check for.
|
||||
# @liquid_syntax_keyword first_expression An expression to be rendered when the variable's value matches `first_value`.
|
||||
# @liquid_syntax_keyword second_expression An expression to be rendered when the variable's value matches `second_value`.
|
||||
# @liquid_syntax_keyword third_expression An expression to be rendered when the variable's value has no match.
|
||||
class Case < Block
|
||||
Syntax = /(#{QuotedFragment})/o
|
||||
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om
|
||||
|
||||
attr_reader :blocks, :left
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
@blocks = []
|
||||
|
||||
if markup =~ Syntax
|
||||
@left = Expression.parse($1)
|
||||
@left = parse_expression(Regexp.last_match(1))
|
||||
else
|
||||
raise SyntaxError.new(options[:locale].t("errors.syntax.case".freeze))
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.case")
|
||||
end
|
||||
end
|
||||
|
||||
def parse(tokens)
|
||||
body = BlockBody.new
|
||||
while more = parse_body(body, tokens)
|
||||
body = @blocks.last.attachment
|
||||
body = case_body = new_body
|
||||
body = @blocks.last.attachment while parse_body(body, tokens)
|
||||
@blocks.reverse_each do |condition|
|
||||
body = condition.attachment
|
||||
unless body.frozen?
|
||||
body.remove_blank_strings if blank?
|
||||
body.freeze
|
||||
end
|
||||
end
|
||||
case_body.freeze
|
||||
end
|
||||
|
||||
def nodelist
|
||||
@ -27,60 +58,71 @@ module Liquid
|
||||
|
||||
def unknown_tag(tag, markup, tokens)
|
||||
case tag
|
||||
when 'when'.freeze
|
||||
when 'when'
|
||||
record_when_condition(markup)
|
||||
when 'else'.freeze
|
||||
when 'else'
|
||||
record_else_condition(markup)
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def render(context)
|
||||
context.stack do
|
||||
execute_else_block = true
|
||||
def render_to_output_buffer(context, output)
|
||||
execute_else_block = true
|
||||
|
||||
output = ''
|
||||
@blocks.each do |block|
|
||||
if block.else?
|
||||
return block.attachment.render(context) if execute_else_block
|
||||
elsif block.evaluate(context)
|
||||
execute_else_block = false
|
||||
output << block.attachment.render(context)
|
||||
end
|
||||
@blocks.each do |block|
|
||||
if block.else?
|
||||
block.attachment.render_to_output_buffer(context, output) if execute_else_block
|
||||
next
|
||||
end
|
||||
|
||||
result = Liquid::Utils.to_liquid_value(
|
||||
block.evaluate(context),
|
||||
)
|
||||
|
||||
if result
|
||||
execute_else_block = false
|
||||
block.attachment.render_to_output_buffer(context, output)
|
||||
end
|
||||
output
|
||||
end
|
||||
|
||||
output
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def record_when_condition(markup)
|
||||
body = BlockBody.new
|
||||
body = new_body
|
||||
|
||||
while markup
|
||||
if not markup =~ WhenSyntax
|
||||
raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_when".freeze))
|
||||
unless markup =~ WhenSyntax
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_when")
|
||||
end
|
||||
|
||||
markup = $2
|
||||
markup = Regexp.last_match(2)
|
||||
|
||||
block = Condition.new(@left, '=='.freeze, Expression.parse($1))
|
||||
block = Condition.new(@left, '==', Condition.parse_expression(parse_context, Regexp.last_match(1)))
|
||||
block.attach(body)
|
||||
@blocks << block
|
||||
end
|
||||
end
|
||||
|
||||
def record_else_condition(markup)
|
||||
if not markup.strip.empty?
|
||||
raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_else".freeze))
|
||||
unless markup.strip.empty?
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_else")
|
||||
end
|
||||
|
||||
block = ElseCondition.new
|
||||
block.attach(BlockBody.new)
|
||||
block.attach(new_body)
|
||||
@blocks << block
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
[@node.left] + @node.blocks
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('case'.freeze, Case)
|
||||
Template.register_tag('case', Case)
|
||||
end
|
||||
|
@ -1,10 +1,25 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category syntax
|
||||
# @liquid_name comment
|
||||
# @liquid_summary
|
||||
# Prevents an expression from being rendered or output.
|
||||
# @liquid_description
|
||||
# Any text inside `comment` tags won't be output, and any Liquid code will be parsed, but not executed.
|
||||
# @liquid_syntax
|
||||
# {% comment %}
|
||||
# content
|
||||
# {% endcomment %}
|
||||
# @liquid_syntax_keyword content The content of the comment.
|
||||
class Comment < Block
|
||||
def render(context)
|
||||
''.freeze
|
||||
def render_to_output_buffer(_context, output)
|
||||
output
|
||||
end
|
||||
|
||||
def unknown_tag(tag, markup, tokens)
|
||||
def unknown_tag(_tag, _markup, _tokens)
|
||||
end
|
||||
|
||||
def blank?
|
||||
@ -12,5 +27,5 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('comment'.freeze, Comment)
|
||||
Template.register_tag('comment', Comment)
|
||||
end
|
||||
|
@ -1,18 +1,22 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# Continue tag to be used to break out of a for loop.
|
||||
#
|
||||
# == Basic Usage:
|
||||
# {% for item in collection %}
|
||||
# {% if item.condition %}
|
||||
# {% continue %}
|
||||
# {% endif %}
|
||||
# {% endfor %}
|
||||
#
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category iteration
|
||||
# @liquid_name continue
|
||||
# @liquid_summary
|
||||
# Causes a [`for` loop](/docs/api/liquid/tags/for) to skip to the next iteration.
|
||||
# @liquid_syntax
|
||||
# {% continue %}
|
||||
class Continue < Tag
|
||||
def interrupt
|
||||
ContinueInterrupt.new
|
||||
INTERRUPT = ContinueInterrupt.new.freeze
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
context.push_interrupt(INTERRUPT)
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('continue'.freeze, Continue)
|
||||
Template.register_tag('continue', Continue)
|
||||
end
|
||||
|
@ -1,46 +1,60 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# Cycle is usually used within a loop to alternate between values, like colors or DOM classes.
|
||||
#
|
||||
# {% for item in items %}
|
||||
# <div class="{% cycle 'red', 'green', 'blue' %}"> {{ item }} </div>
|
||||
# {% end %}
|
||||
#
|
||||
# <div class="red"> Item one </div>
|
||||
# <div class="green"> Item two </div>
|
||||
# <div class="blue"> Item three </div>
|
||||
# <div class="red"> Item four </div>
|
||||
# <div class="green"> Item five</div>
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category iteration
|
||||
# @liquid_name cycle
|
||||
# @liquid_summary
|
||||
# Loops through a group of strings and outputs them one at a time for each iteration of a [`for` loop](/docs/api/liquid/tags/for).
|
||||
# @liquid_description
|
||||
# The `cycle` tag must be used inside a `for` loop.
|
||||
#
|
||||
# > Tip:
|
||||
# > Use the `cycle` tag to output text in a predictable pattern. For example, to apply odd/even classes to rows in a table.
|
||||
# @liquid_syntax
|
||||
# {% cycle string, string, ... %}
|
||||
class Cycle < Tag
|
||||
SimpleSyntax = /\A#{QuotedFragment}+/o
|
||||
NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om
|
||||
|
||||
attr_reader :variables
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
case markup
|
||||
when NamedSyntax
|
||||
@variables = variables_from_string($2)
|
||||
@name = Expression.parse($1)
|
||||
@variables = variables_from_string(Regexp.last_match(2))
|
||||
@name = parse_expression(Regexp.last_match(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))
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.cycle")
|
||||
end
|
||||
end
|
||||
|
||||
def render(context)
|
||||
context.registers[:cycle] ||= Hash.new(0)
|
||||
def render_to_output_buffer(context, output)
|
||||
context.registers[:cycle] ||= {}
|
||||
|
||||
context.stack do
|
||||
key = context.evaluate(@name)
|
||||
iteration = context.registers[:cycle][key]
|
||||
result = context.evaluate(@variables[iteration])
|
||||
iteration += 1
|
||||
iteration = 0 if iteration >= @variables.size
|
||||
context.registers[:cycle][key] = iteration
|
||||
result
|
||||
key = context.evaluate(@name)
|
||||
iteration = context.registers[:cycle][key].to_i
|
||||
|
||||
val = context.evaluate(@variables[iteration])
|
||||
|
||||
if val.is_a?(Array)
|
||||
val = val.join
|
||||
elsif !val.is_a?(String)
|
||||
val = val.to_s
|
||||
end
|
||||
|
||||
output << val
|
||||
|
||||
iteration += 1
|
||||
iteration = 0 if iteration >= @variables.size
|
||||
|
||||
context.registers[:cycle][key] = iteration
|
||||
output
|
||||
end
|
||||
|
||||
private
|
||||
@ -48,9 +62,15 @@ module Liquid
|
||||
def variables_from_string(markup)
|
||||
markup.split(',').collect do |var|
|
||||
var =~ /\s*(#{QuotedFragment})\s*/o
|
||||
$1 ? Expression.parse($1) : nil
|
||||
Regexp.last_match(1) ? parse_expression(Regexp.last_match(1)) : nil
|
||||
end.compact
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
Array(@node.variables)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('cycle', Cycle)
|
||||
|
@ -1,38 +1,40 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
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.
|
||||
# NOTE: decrement is a pre-decrement, --i,
|
||||
# while increment is post: i++.
|
||||
#
|
||||
# (To achieve the survival, the application must keep the context)
|
||||
#
|
||||
# if the variable does not exist, it is created with value 0.
|
||||
|
||||
# Hello: {% decrement variable %}
|
||||
#
|
||||
# gives you:
|
||||
#
|
||||
# Hello: -1
|
||||
# Hello: -2
|
||||
# Hello: -3
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category variable
|
||||
# @liquid_name decrement
|
||||
# @liquid_summary
|
||||
# Creates a new variable, with a default value of -1, that's decreased by 1 with each subsequent call.
|
||||
# @liquid_description
|
||||
# Variables that are declared with `decrement` are unique to the [layout](/themes/architecture/layouts), [template](/themes/architecture/templates),
|
||||
# or [section](/themes/architecture/sections) file that they're created in. However, the variable is shared across
|
||||
# [snippets](/themes/architecture#snippets) included in the file.
|
||||
#
|
||||
# Similarly, variables that are created with `decrement` are independent from those created with [`assign`](/docs/api/liquid/tags/assign)
|
||||
# and [`capture`](/docs/api/liquid/tags/capture). However, `decrement` and [`increment`](/docs/api/liquid/tags/increment) share
|
||||
# variables.
|
||||
# @liquid_syntax
|
||||
# {% decrement variable_name %}
|
||||
# @liquid_syntax_keyword variable_name The name of the variable being decremented.
|
||||
class Decrement < Tag
|
||||
attr_reader :variable_name
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
@variable = markup.strip
|
||||
@variable_name = markup.strip
|
||||
end
|
||||
|
||||
def render(context)
|
||||
value = context.environments.first[@variable] ||= 0
|
||||
value = value - 1
|
||||
context.environments.first[@variable] = value
|
||||
value.to_s
|
||||
def render_to_output_buffer(context, output)
|
||||
counter_environment = context.environments.first
|
||||
value = counter_environment[@variable_name] || 0
|
||||
value -= 1
|
||||
counter_environment[@variable_name] = value
|
||||
output << value.to_s
|
||||
output
|
||||
end
|
||||
|
||||
private
|
||||
end
|
||||
|
||||
Template.register_tag('decrement'.freeze, Decrement)
|
||||
Template.register_tag('decrement', Decrement)
|
||||
end
|
||||
|
41
lib/liquid/tags/echo.rb
Normal file
41
lib/liquid/tags/echo.rb
Normal file
@ -0,0 +1,41 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category syntax
|
||||
# @liquid_name echo
|
||||
# @liquid_summary
|
||||
# Outputs an expression.
|
||||
# @liquid_description
|
||||
# Using the `echo` tag is the same as wrapping an expression in curly brackets (`{{` and `}}`). However, unlike the curly
|
||||
# bracket method, you can use the `echo` tag inside [`liquid` tags](/docs/api/liquid/tags/liquid).
|
||||
#
|
||||
# > Tip:
|
||||
# > You can use [filters](/docs/api/liquid/filters) on expressions inside `echo` tags.
|
||||
# @liquid_syntax
|
||||
# {% liquid
|
||||
# echo expression
|
||||
# %}
|
||||
# @liquid_syntax_keyword expression The expression to be output.
|
||||
class Echo < Tag
|
||||
attr_reader :variable
|
||||
|
||||
def initialize(tag_name, markup, parse_context)
|
||||
super
|
||||
@variable = Variable.new(markup, parse_context)
|
||||
end
|
||||
|
||||
def render(context)
|
||||
@variable.render_to_output_buffer(context, +'')
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
[@node.variable]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('echo', Echo)
|
||||
end
|
@ -1,61 +1,52 @@
|
||||
module Liquid
|
||||
# frozen_string_literal: true
|
||||
|
||||
# "For" iterates over an array or collection.
|
||||
# Several useful variables are available to you within the loop.
|
||||
#
|
||||
# == Basic usage:
|
||||
# {% for item in collection %}
|
||||
# {{ forloop.index }}: {{ item.name }}
|
||||
# {% endfor %}
|
||||
#
|
||||
# == Advanced usage:
|
||||
# {% for item in collection %}
|
||||
# <div {% if forloop.first %}class="first"{% endif %}>
|
||||
# Item {{ forloop.index }}: {{ item.name }}
|
||||
# </div>
|
||||
# {% else %}
|
||||
# There is nothing in the collection.
|
||||
# {% endfor %}
|
||||
#
|
||||
# You can also define a limit and offset much like SQL. Remember
|
||||
# that offset starts at 0 for the first item.
|
||||
#
|
||||
# {% for item in collection limit:5 offset:10 %}
|
||||
# {{ item.name }}
|
||||
# {% end %}
|
||||
#
|
||||
# To reverse the for loop simply use {% for item in collection reversed %}
|
||||
#
|
||||
# == Available variables:
|
||||
#
|
||||
# forloop.name:: 'item-collection'
|
||||
# forloop.length:: Length of the loop
|
||||
# forloop.index:: The current item's position in the collection;
|
||||
# forloop.index starts at 1.
|
||||
# This is helpful for non-programmers who start believe
|
||||
# the first item in an array is 1, not 0.
|
||||
# forloop.index0:: The current item's position in the collection
|
||||
# where the first item is 0
|
||||
# forloop.rindex:: Number of items remaining in the loop
|
||||
# (length - index) where 1 is the last item.
|
||||
# forloop.rindex0:: Number of items remaining in the loop
|
||||
# where 0 is the last item.
|
||||
# forloop.first:: Returns true if the item is the first item.
|
||||
# forloop.last:: Returns true if the item is the last item.
|
||||
module Liquid
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category iteration
|
||||
# @liquid_name for
|
||||
# @liquid_summary
|
||||
# Renders an expression for every item in an array.
|
||||
# @liquid_description
|
||||
# You can do a maximum of 50 iterations with a `for` loop. If you need to iterate over more than 50 items, then use the
|
||||
# [`paginate` tag](/docs/api/liquid/tags/paginate) to split the items over multiple pages.
|
||||
#
|
||||
# > Tip:
|
||||
# > Every `for` loop has an associated [`forloop` object](/docs/api/liquid/objects/forloop) with information about the loop.
|
||||
# @liquid_syntax
|
||||
# {% for variable in array %}
|
||||
# expression
|
||||
# {% endfor %}
|
||||
# @liquid_syntax_keyword variable The current item in the array.
|
||||
# @liquid_syntax_keyword array The array to iterate over.
|
||||
# @liquid_syntax_keyword expression The expression to render for each iteration.
|
||||
# @liquid_optional_param limit [number] The number of iterations to perform.
|
||||
# @liquid_optional_param offset [number] The 1-based index to start iterating at.
|
||||
# @liquid_optional_param range [untyped] A custom numeric range to iterate over.
|
||||
# @liquid_optional_param reversed [untyped] Iterate in reverse order.
|
||||
class For < Block
|
||||
Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
|
||||
|
||||
attr_reader :collection_name, :variable_name, :limit, :from
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
@from = @limit = nil
|
||||
parse_with_selected_parser(markup)
|
||||
@for_block = BlockBody.new
|
||||
@for_block = new_body
|
||||
@else_block = nil
|
||||
end
|
||||
|
||||
def parse(tokens)
|
||||
if more = parse_body(@for_block, tokens)
|
||||
if parse_body(@for_block, tokens)
|
||||
parse_body(@else_block, tokens)
|
||||
end
|
||||
if blank?
|
||||
@else_block&.remove_blank_strings
|
||||
@for_block.remove_blank_strings
|
||||
end
|
||||
@else_block&.freeze
|
||||
@for_block.freeze
|
||||
end
|
||||
|
||||
def nodelist
|
||||
@ -63,99 +54,56 @@ module Liquid
|
||||
end
|
||||
|
||||
def unknown_tag(tag, markup, tokens)
|
||||
return super unless tag == 'else'.freeze
|
||||
@else_block = BlockBody.new
|
||||
return super unless tag == 'else'
|
||||
@else_block = new_body
|
||||
end
|
||||
|
||||
def render(context)
|
||||
context.registers[:for] ||= Hash.new(0)
|
||||
def render_to_output_buffer(context, output)
|
||||
segment = collection_segment(context)
|
||||
|
||||
collection = context.evaluate(@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 @from == :continue
|
||||
context.registers[:for][@name].to_i
|
||||
if segment.empty?
|
||||
render_else(context, output)
|
||||
else
|
||||
context.evaluate(@from).to_i
|
||||
render_segment(context, output, segment)
|
||||
end
|
||||
|
||||
limit = context.evaluate(@limit)
|
||||
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 << @for_block.render(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
|
||||
output
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def lax_parse(markup)
|
||||
if markup =~ Syntax
|
||||
@variable_name = $1
|
||||
collection_name = $2
|
||||
@reversed = $3
|
||||
@name = "#{@variable_name}-#{collection_name}"
|
||||
@collection_name = Expression.parse(collection_name)
|
||||
@variable_name = Regexp.last_match(1)
|
||||
collection_name = Regexp.last_match(2)
|
||||
@reversed = !!Regexp.last_match(3)
|
||||
@name = "#{@variable_name}-#{collection_name}"
|
||||
@collection_name = parse_expression(collection_name)
|
||||
markup.scan(TagAttributes) do |key, value|
|
||||
set_attribute(key, value)
|
||||
end
|
||||
else
|
||||
raise SyntaxError.new(options[:locale].t("errors.syntax.for".freeze))
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.for")
|
||||
end
|
||||
end
|
||||
|
||||
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}"
|
||||
@collection_name = Expression.parse(collection_name)
|
||||
@reversed = p.id?('reversed'.freeze)
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in')
|
||||
|
||||
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))
|
||||
collection_name = p.expression
|
||||
@collection_name = parse_expression(collection_name)
|
||||
|
||||
@name = "#{@variable_name}-#{collection_name}"
|
||||
@reversed = p.id?('reversed')
|
||||
|
||||
while p.look(:comma) || p.look(:id)
|
||||
p.consume?(:comma)
|
||||
unless (attribute = p.id?('limit') || p.id?('offset'))
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_attribute")
|
||||
end
|
||||
p.consume
|
||||
p.consume(:colon)
|
||||
set_attribute(attribute, p.expression)
|
||||
end
|
||||
p.consume(:end_of_string)
|
||||
@ -163,27 +111,96 @@ module Liquid
|
||||
|
||||
private
|
||||
|
||||
def collection_segment(context)
|
||||
offsets = context.registers[:for] ||= {}
|
||||
|
||||
from = if @from == :continue
|
||||
offsets[@name].to_i
|
||||
else
|
||||
from_value = context.evaluate(@from)
|
||||
if from_value.nil?
|
||||
0
|
||||
else
|
||||
Utils.to_integer(from_value)
|
||||
end
|
||||
end
|
||||
|
||||
collection = context.evaluate(@collection_name)
|
||||
collection = collection.to_a if collection.is_a?(Range)
|
||||
|
||||
limit_value = context.evaluate(@limit)
|
||||
to = if limit_value.nil?
|
||||
nil
|
||||
else
|
||||
Utils.to_integer(limit_value) + from
|
||||
end
|
||||
|
||||
segment = Utils.slice_collection(collection, from, to)
|
||||
segment.reverse! if @reversed
|
||||
|
||||
offsets[@name] = from + segment.length
|
||||
|
||||
segment
|
||||
end
|
||||
|
||||
def render_segment(context, output, segment)
|
||||
for_stack = context.registers[:for_stack] ||= []
|
||||
length = segment.length
|
||||
|
||||
context.stack do
|
||||
loop_vars = Liquid::ForloopDrop.new(@name, length, for_stack[-1])
|
||||
|
||||
for_stack.push(loop_vars)
|
||||
|
||||
begin
|
||||
context['forloop'] = loop_vars
|
||||
|
||||
segment.each do |item|
|
||||
context[@variable_name] = item
|
||||
@for_block.render_to_output_buffer(context, output)
|
||||
loop_vars.send(:increment!)
|
||||
|
||||
# Handle any interrupts if they exist.
|
||||
next unless context.interrupt?
|
||||
interrupt = context.pop_interrupt
|
||||
break if interrupt.is_a?(BreakInterrupt)
|
||||
next if interrupt.is_a?(ContinueInterrupt)
|
||||
end
|
||||
ensure
|
||||
for_stack.pop
|
||||
end
|
||||
end
|
||||
|
||||
output
|
||||
end
|
||||
|
||||
def set_attribute(key, expr)
|
||||
case key
|
||||
when 'offset'.freeze
|
||||
@from = if expr == 'continue'.freeze
|
||||
when 'offset'
|
||||
@from = if expr == 'continue'
|
||||
:continue
|
||||
else
|
||||
Expression.parse(expr)
|
||||
parse_expression(expr)
|
||||
end
|
||||
when 'limit'.freeze
|
||||
@limit = Expression.parse(expr)
|
||||
when 'limit'
|
||||
@limit = parse_expression(expr)
|
||||
end
|
||||
end
|
||||
|
||||
def render_else(context)
|
||||
@else_block ? @else_block.render(context) : ''.freeze
|
||||
def render_else(context, output)
|
||||
if @else_block
|
||||
@else_block.render_to_output_buffer(context, output)
|
||||
else
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
def iterable?(collection)
|
||||
collection.respond_to?(:each) || Utils.non_blank_string?(collection)
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
(super + [@node.limit, @node.from, @node.collection_name]).compact
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('for'.freeze, For)
|
||||
Template.register_tag('for', For)
|
||||
end
|
||||
|
@ -1,111 +1,140 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# If is the conditional block
|
||||
#
|
||||
# {% if user.admin %}
|
||||
# Admin user!
|
||||
# {% else %}
|
||||
# Not admin user
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category conditional
|
||||
# @liquid_name if
|
||||
# @liquid_summary
|
||||
# Renders an expression if a specific condition is `true`.
|
||||
# @liquid_syntax
|
||||
# {% if condition %}
|
||||
# expression
|
||||
# {% endif %}
|
||||
#
|
||||
# There are {% if count < 5 %} less {% else %} more {% endif %} items than you need.
|
||||
#
|
||||
# @liquid_syntax_keyword condition The condition to evaluate.
|
||||
# @liquid_syntax_keyword expression The expression to render if the condition is met.
|
||||
class If < Block
|
||||
Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o
|
||||
Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o
|
||||
ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o
|
||||
BOOLEAN_OPERATORS = %w(and or)
|
||||
BOOLEAN_OPERATORS = %w(and or).freeze
|
||||
|
||||
attr_reader :blocks
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
@blocks = []
|
||||
push_block('if'.freeze, markup)
|
||||
end
|
||||
|
||||
def parse(tokens)
|
||||
while more = parse_body(@blocks.last.attachment, tokens)
|
||||
end
|
||||
push_block('if', markup)
|
||||
end
|
||||
|
||||
def nodelist
|
||||
@blocks.map(&:attachment)
|
||||
end
|
||||
|
||||
def parse(tokens)
|
||||
while parse_body(@blocks.last.attachment, tokens)
|
||||
end
|
||||
@blocks.reverse_each do |block|
|
||||
block.attachment.remove_blank_strings if blank?
|
||||
block.attachment.freeze
|
||||
end
|
||||
end
|
||||
|
||||
ELSE_TAG_NAMES = ['elsif', 'else'].freeze
|
||||
private_constant :ELSE_TAG_NAMES
|
||||
|
||||
def unknown_tag(tag, markup, tokens)
|
||||
if ['elsif'.freeze, 'else'.freeze].include?(tag)
|
||||
if ELSE_TAG_NAMES.include?(tag)
|
||||
push_block(tag, markup)
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def render(context)
|
||||
context.stack do
|
||||
@blocks.each do |block|
|
||||
if block.evaluate(context)
|
||||
return block.attachment.render(context)
|
||||
end
|
||||
def render_to_output_buffer(context, output)
|
||||
@blocks.each do |block|
|
||||
result = Liquid::Utils.to_liquid_value(
|
||||
block.evaluate(context),
|
||||
)
|
||||
|
||||
if result
|
||||
return block.attachment.render_to_output_buffer(context, output)
|
||||
end
|
||||
''.freeze
|
||||
end
|
||||
|
||||
output
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def push_block(tag, markup)
|
||||
block = if tag == 'else'.freeze
|
||||
ElseCondition.new
|
||||
else
|
||||
parse_with_selected_parser(markup)
|
||||
end
|
||||
|
||||
@blocks.push(block)
|
||||
block.attach(BlockBody.new)
|
||||
def push_block(tag, markup)
|
||||
block = if tag == 'else'
|
||||
ElseCondition.new
|
||||
else
|
||||
parse_with_selected_parser(markup)
|
||||
end
|
||||
|
||||
def lax_parse(markup)
|
||||
expressions = markup.scan(ExpressionsAndOperators)
|
||||
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop =~ Syntax
|
||||
@blocks.push(block)
|
||||
block.attach(new_body)
|
||||
end
|
||||
|
||||
condition = Condition.new(Expression.parse($1), $2, Expression.parse($3))
|
||||
def parse_expression(markup)
|
||||
Condition.parse_expression(parse_context, markup)
|
||||
end
|
||||
|
||||
while not expressions.empty?
|
||||
operator = expressions.pop.to_s.strip
|
||||
def lax_parse(markup)
|
||||
expressions = markup.scan(ExpressionsAndOperators)
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop =~ Syntax
|
||||
|
||||
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop.to_s =~ Syntax
|
||||
condition = Condition.new(parse_expression(Regexp.last_match(1)), Regexp.last_match(2), parse_expression(Regexp.last_match(3)))
|
||||
|
||||
new_condition = Condition.new(Expression.parse($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
|
||||
until expressions.empty?
|
||||
operator = expressions.pop.to_s.strip
|
||||
|
||||
condition
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop.to_s =~ Syntax
|
||||
|
||||
new_condition = Condition.new(parse_expression(Regexp.last_match(1)), Regexp.last_match(2), parse_expression(Regexp.last_match(3)))
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.if") 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_comparisons(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_comparisons(p)
|
||||
condition = parse_comparison(p)
|
||||
first_condition = condition
|
||||
while (op = (p.id?('and') || p.id?('or')))
|
||||
child_condition = parse_comparison(p)
|
||||
condition.send(op, child_condition)
|
||||
condition = child_condition
|
||||
end
|
||||
first_condition
|
||||
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
|
||||
def parse_comparison(p)
|
||||
a = parse_expression(p.expression)
|
||||
if (op = p.consume?(:comparison))
|
||||
b = parse_expression(p.expression)
|
||||
Condition.new(a, op, b)
|
||||
else
|
||||
Condition.new(a)
|
||||
end
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
@node.blocks
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('if'.freeze, If)
|
||||
Template.register_tag('if', If)
|
||||
end
|
||||
|
@ -1,20 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Ifchanged < Block
|
||||
def render_to_output_buffer(context, output)
|
||||
block_output = +''
|
||||
super(context, block_output)
|
||||
|
||||
def render(context)
|
||||
context.stack do
|
||||
|
||||
output = super
|
||||
|
||||
if output != context.registers[:ifchanged]
|
||||
context.registers[:ifchanged] = output
|
||||
output
|
||||
else
|
||||
''.freeze
|
||||
end
|
||||
if block_output != context.registers[:ifchanged]
|
||||
context.registers[:ifchanged] = block_output
|
||||
output << block_output
|
||||
end
|
||||
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('ifchanged'.freeze, Ifchanged)
|
||||
Template.register_tag('ifchanged', Ifchanged)
|
||||
end
|
||||
|
@ -1,107 +1,115 @@
|
||||
module Liquid
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Include allows templates to relate with other templates
|
||||
#
|
||||
# Simply include another template:
|
||||
#
|
||||
# {% include 'product' %}
|
||||
#
|
||||
# Include a template with a local variable:
|
||||
#
|
||||
# {% include 'product' with products[0] %}
|
||||
#
|
||||
# Include a template for a collection:
|
||||
#
|
||||
# {% include 'product' for products %}
|
||||
module Liquid
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category theme
|
||||
# @liquid_name include
|
||||
# @liquid_summary
|
||||
# Renders a [snippet](/themes/architecture#snippets).
|
||||
# @liquid_description
|
||||
# Inside the snippet, you can access and alter variables that are [created](/docs/api/liquid/tags/variable-tags) outside of the
|
||||
# snippet.
|
||||
# @liquid_syntax
|
||||
# {% include 'filename' %}
|
||||
# @liquid_syntax_keyword filename The name of the snippet to render, without the `.liquid` extension.
|
||||
# @liquid_deprecated
|
||||
# Deprecated because the way that variables are handled reduces performance and makes code harder to both read and maintain.
|
||||
#
|
||||
# The `include` tag has been replaced by [`render`](/docs/api/liquid/tags/render).
|
||||
class Include < Tag
|
||||
Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/o
|
||||
prepend Tag::Disableable
|
||||
|
||||
SYNTAX = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
|
||||
Syntax = SYNTAX
|
||||
|
||||
attr_reader :template_name_expr, :variable_name_expr, :attributes
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
|
||||
if markup =~ Syntax
|
||||
if markup =~ SYNTAX
|
||||
|
||||
template_name = $1
|
||||
variable_name = $3
|
||||
template_name = Regexp.last_match(1)
|
||||
variable_name = Regexp.last_match(3)
|
||||
|
||||
@variable_name = Expression.parse(variable_name || template_name[1..-2])
|
||||
@context_variable_name = template_name[1..-2].split('/'.freeze).last
|
||||
@template_name = Expression.parse(template_name)
|
||||
@attributes = {}
|
||||
@alias_name = Regexp.last_match(5)
|
||||
@variable_name_expr = variable_name ? parse_expression(variable_name) : nil
|
||||
@template_name_expr = parse_expression(template_name)
|
||||
@attributes = {}
|
||||
|
||||
markup.scan(TagAttributes) do |key, value|
|
||||
@attributes[key] = Expression.parse(value)
|
||||
@attributes[key] = parse_expression(value)
|
||||
end
|
||||
|
||||
else
|
||||
raise SyntaxError.new(options[:locale].t("errors.syntax.include".freeze))
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.include")
|
||||
end
|
||||
end
|
||||
|
||||
def parse(tokens)
|
||||
def parse(_tokens)
|
||||
end
|
||||
|
||||
def render(context)
|
||||
partial = load_cached_partial(context)
|
||||
variable = context.evaluate(@variable_name)
|
||||
def render_to_output_buffer(context, output)
|
||||
template_name = context.evaluate(@template_name_expr)
|
||||
raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name.is_a?(String)
|
||||
|
||||
context.stack do
|
||||
@attributes.each do |key, value|
|
||||
context[key] = context.evaluate(value)
|
||||
end
|
||||
partial = PartialCache.load(
|
||||
template_name,
|
||||
context: context,
|
||||
parse_context: parse_context,
|
||||
)
|
||||
|
||||
if variable.is_a?(Array)
|
||||
variable.collect do |var|
|
||||
context[@context_variable_name] = var
|
||||
partial.render(context)
|
||||
context_variable_name = @alias_name || template_name.split('/').last
|
||||
|
||||
variable = if @variable_name_expr
|
||||
context.evaluate(@variable_name_expr)
|
||||
else
|
||||
context.find_variable(template_name, raise_on_not_found: false)
|
||||
end
|
||||
|
||||
old_template_name = context.template_name
|
||||
old_partial = context.partial
|
||||
|
||||
begin
|
||||
context.template_name = partial.name
|
||||
context.partial = true
|
||||
|
||||
context.stack do
|
||||
@attributes.each do |key, value|
|
||||
context[key] = context.evaluate(value)
|
||||
end
|
||||
|
||||
if variable.is_a?(Array)
|
||||
variable.each do |var|
|
||||
context[context_variable_name] = var
|
||||
partial.render_to_output_buffer(context, output)
|
||||
end
|
||||
else
|
||||
context[context_variable_name] = variable
|
||||
partial.render_to_output_buffer(context, output)
|
||||
end
|
||||
else
|
||||
context[@context_variable_name] = variable
|
||||
partial.render(context)
|
||||
end
|
||||
ensure
|
||||
context.template_name = old_template_name
|
||||
context.partial = old_partial
|
||||
end
|
||||
|
||||
output
|
||||
end
|
||||
|
||||
private
|
||||
def load_cached_partial(context)
|
||||
cached_partials = context.registers[:cached_partials] || {}
|
||||
template_name = context.evaluate(@template_name)
|
||||
alias_method :parse_context, :options
|
||||
private :parse_context
|
||||
|
||||
if cached = cached_partials[template_name]
|
||||
return cached
|
||||
end
|
||||
source = read_template_from_file_system(context)
|
||||
partial = Liquid::Template.parse(source, pass_options)
|
||||
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
|
||||
|
||||
# make read_template_file call backwards-compatible.
|
||||
case file_system.method(:read_template_file).arity
|
||||
when 1
|
||||
file_system.read_template_file(context.evaluate(@template_name))
|
||||
when 2
|
||||
file_system.read_template_file(context.evaluate(@template_name), context)
|
||||
else
|
||||
raise ArgumentError, "file_system.read_template_file expects two parameters: (template_name, context)"
|
||||
end
|
||||
end
|
||||
|
||||
def pass_options
|
||||
dont_pass = @options[:include_options_blacklist]
|
||||
return {locale: @options[:locale]} if dont_pass == true
|
||||
opts = @options.merge(included: true, include_options_blacklist: false)
|
||||
if dont_pass.is_a?(Array)
|
||||
dont_pass.each {|o| opts.delete(o)}
|
||||
end
|
||||
opts
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
[
|
||||
@node.template_name_expr,
|
||||
@node.variable_name_expr,
|
||||
] + @node.attributes.values
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('include'.freeze, Include)
|
||||
Template.register_tag('include', Include)
|
||||
end
|
||||
|
@ -1,31 +1,40 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# increment 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.
|
||||
# (To achieve the survival, the application must keep the context)
|
||||
#
|
||||
# if the variable does not exist, it is created with value 0.
|
||||
#
|
||||
# Hello: {% increment variable %}
|
||||
#
|
||||
# gives you:
|
||||
#
|
||||
# Hello: 0
|
||||
# Hello: 1
|
||||
# Hello: 2
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category variable
|
||||
# @liquid_name increment
|
||||
# @liquid_summary
|
||||
# Creates a new variable, with a default value of 0, that's increased by 1 with each subsequent call.
|
||||
# @liquid_description
|
||||
# Variables that are declared with `increment` are unique to the [layout](/themes/architecture/layouts), [template](/themes/architecture/templates),
|
||||
# or [section](/themes/architecture/sections) file that they're created in. However, the variable is shared across
|
||||
# [snippets](/themes/architecture#snippets) included in the file.
|
||||
#
|
||||
# Similarly, variables that are created with `increment` are independent from those created with [`assign`](/docs/api/liquid/tags/assign)
|
||||
# and [`capture`](/docs/api/liquid/tags/capture). However, `increment` and [`decrement`](/docs/api/liquid/tags/decrement) share
|
||||
# variables.
|
||||
# @liquid_syntax
|
||||
# {% increment variable_name %}
|
||||
# @liquid_syntax_keyword variable_name The name of the variable being incremented.
|
||||
class Increment < Tag
|
||||
attr_reader :variable_name
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
@variable = markup.strip
|
||||
@variable_name = markup.strip
|
||||
end
|
||||
|
||||
def render(context)
|
||||
value = context.environments.first[@variable] ||= 0
|
||||
context.environments.first[@variable] = value + 1
|
||||
value.to_s
|
||||
def render_to_output_buffer(context, output)
|
||||
counter_environment = context.environments.first
|
||||
value = counter_environment[@variable_name] || 0
|
||||
counter_environment[@variable_name] = value + 1
|
||||
|
||||
output << value.to_s
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('increment'.freeze, Increment)
|
||||
Template.register_tag('increment', Increment)
|
||||
end
|
||||
|
30
lib/liquid/tags/inline_comment.rb
Normal file
30
lib/liquid/tags/inline_comment.rb
Normal file
@ -0,0 +1,30 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class InlineComment < Tag
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
|
||||
# Semantically, a comment should only ignore everything after it on the line.
|
||||
# Currently, this implementation doesn't support mixing a comment with another tag
|
||||
# but we need to reserve future support for this and prevent the introduction
|
||||
# of inline comments from being backward incompatible change.
|
||||
#
|
||||
# As such, we're forcing users to put a # symbol on every line otherwise this
|
||||
# tag will throw an error.
|
||||
if markup.match?(/\n\s*[^#\s]/)
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.inline_comment_invalid")
|
||||
end
|
||||
end
|
||||
|
||||
def render_to_output_buffer(_context, output)
|
||||
output
|
||||
end
|
||||
|
||||
def blank?
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('#', InlineComment)
|
||||
end
|
@ -1,20 +1,44 @@
|
||||
module Liquid
|
||||
class Raw < Block
|
||||
FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
|
||||
# frozen_string_literal: true
|
||||
|
||||
def parse(tokens)
|
||||
@body = ''
|
||||
while token = tokens.shift
|
||||
if token =~ FullTokenPossiblyInvalid
|
||||
@body << $1 if $1 != "".freeze
|
||||
return if block_delimiter == $2
|
||||
end
|
||||
@body << token if not token.empty?
|
||||
end
|
||||
module Liquid
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category syntax
|
||||
# @liquid_name raw
|
||||
# @liquid_summary
|
||||
# Outputs any Liquid code as text instead of rendering it.
|
||||
# @liquid_syntax
|
||||
# {% raw %}
|
||||
# expression
|
||||
# {% endraw %}
|
||||
# @liquid_syntax_keyword expression The expression to be output without being rendered.
|
||||
class Raw < Block
|
||||
Syntax = /\A\s*\z/
|
||||
FullTokenPossiblyInvalid = /\A(.*)#{TagStart}#{WhitespaceControl}?\s*(\w+)\s*(.*)?#{WhitespaceControl}?#{TagEnd}\z/om
|
||||
|
||||
def initialize(tag_name, markup, parse_context)
|
||||
super
|
||||
|
||||
ensure_valid_markup(tag_name, markup, parse_context)
|
||||
end
|
||||
|
||||
def render(context)
|
||||
@body
|
||||
def parse(tokens)
|
||||
@body = +''
|
||||
while (token = tokens.shift)
|
||||
if token =~ FullTokenPossiblyInvalid && block_delimiter == Regexp.last_match(2)
|
||||
parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
|
||||
@body << Regexp.last_match(1) if Regexp.last_match(1) != ""
|
||||
return
|
||||
end
|
||||
@body << token unless token.empty?
|
||||
end
|
||||
|
||||
raise_tag_never_closed(block_name)
|
||||
end
|
||||
|
||||
def render_to_output_buffer(_context, output)
|
||||
output << @body
|
||||
output
|
||||
end
|
||||
|
||||
def nodelist
|
||||
@ -24,7 +48,15 @@ module Liquid
|
||||
def blank?
|
||||
@body.empty?
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def ensure_valid_markup(tag_name, markup, parse_context)
|
||||
unless Syntax.match?(markup)
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_unexpected_args", tag: tag_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('raw'.freeze, Raw)
|
||||
Template.register_tag('raw', Raw)
|
||||
end
|
||||
|
113
lib/liquid/tags/render.rb
Normal file
113
lib/liquid/tags/render.rb
Normal file
@ -0,0 +1,113 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category theme
|
||||
# @liquid_name render
|
||||
# @liquid_summary
|
||||
# Renders a [snippet](/themes/architecture#snippets) or [app block](/themes/architecture/sections/section-schema#render-app-blocks).
|
||||
# @liquid_description
|
||||
# Inside snippets and app blocks, you can't directly access variables that are [created](/docs/api/liquid/tags/variable-tags) outside
|
||||
# of the snippet or app block. However, you can [specify variables as parameters](/docs/api/liquid/tags/render#render-passing-variables-to-a-snippet)
|
||||
# to pass outside variables to snippets.
|
||||
#
|
||||
# While you can't directly access created variables, you can access global objects, as well as any objects that are
|
||||
# directly accessible outside the snippet or app block. For example, a snippet or app block inside the [product template](/themes/architecture/templates/product)
|
||||
# can access the [`product` object](/docs/api/liquid/objects/product), and a snippet or app block inside a [section](/themes/architecture/sections)
|
||||
# can access the [`section` object](/docs/api/liquid/objects/section).
|
||||
#
|
||||
# Outside a snippet or app block, you can't access variables created inside the snippet or app block.
|
||||
#
|
||||
# > Note:
|
||||
# > When you render a snippet using the `render` tag, you can't use the [`include` tag](/docs/api/liquid/tags/include)
|
||||
# > inside the snippet.
|
||||
# @liquid_syntax
|
||||
# {% render 'filename' %}
|
||||
# @liquid_syntax_keyword filename The name of the snippet to render, without the `.liquid` extension.
|
||||
class Render < Tag
|
||||
FOR = 'for'
|
||||
SYNTAX = /(#{QuotedString}+)(\s+(with|#{FOR})\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
|
||||
|
||||
disable_tags "include"
|
||||
|
||||
attr_reader :template_name_expr, :variable_name_expr, :attributes, :alias_name
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.render") unless markup =~ SYNTAX
|
||||
|
||||
template_name = Regexp.last_match(1)
|
||||
with_or_for = Regexp.last_match(3)
|
||||
variable_name = Regexp.last_match(4)
|
||||
|
||||
@alias_name = Regexp.last_match(6)
|
||||
@variable_name_expr = variable_name ? parse_expression(variable_name) : nil
|
||||
@template_name_expr = parse_expression(template_name)
|
||||
@is_for_loop = (with_or_for == FOR)
|
||||
|
||||
@attributes = {}
|
||||
markup.scan(TagAttributes) do |key, value|
|
||||
@attributes[key] = parse_expression(value)
|
||||
end
|
||||
end
|
||||
|
||||
def for_loop?
|
||||
@is_for_loop
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
render_tag(context, output)
|
||||
end
|
||||
|
||||
def render_tag(context, output)
|
||||
# The expression should be a String literal, which parses to a String object
|
||||
template_name = @template_name_expr
|
||||
raise ::ArgumentError unless template_name.is_a?(String)
|
||||
|
||||
partial = PartialCache.load(
|
||||
template_name,
|
||||
context: context,
|
||||
parse_context: parse_context,
|
||||
)
|
||||
|
||||
context_variable_name = @alias_name || template_name.split('/').last
|
||||
|
||||
render_partial_func = ->(var, forloop) {
|
||||
inner_context = context.new_isolated_subcontext
|
||||
inner_context.template_name = partial.name
|
||||
inner_context.partial = true
|
||||
inner_context['forloop'] = forloop if forloop
|
||||
|
||||
@attributes.each do |key, value|
|
||||
inner_context[key] = context.evaluate(value)
|
||||
end
|
||||
inner_context[context_variable_name] = var unless var.nil?
|
||||
partial.render_to_output_buffer(inner_context, output)
|
||||
forloop&.send(:increment!)
|
||||
}
|
||||
|
||||
variable = @variable_name_expr ? context.evaluate(@variable_name_expr) : nil
|
||||
if @is_for_loop && variable.respond_to?(:each) && variable.respond_to?(:count)
|
||||
forloop = Liquid::ForloopDrop.new(template_name, variable.count, nil)
|
||||
variable.each { |var| render_partial_func.call(var, forloop) }
|
||||
else
|
||||
render_partial_func.call(variable, nil)
|
||||
end
|
||||
|
||||
output
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
[
|
||||
@node.template_name_expr,
|
||||
@node.variable_name_expr,
|
||||
] + @node.attributes.values
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('render', Render)
|
||||
end
|
@ -1,73 +1,96 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category iteration
|
||||
# @liquid_name tablerow
|
||||
# @liquid_summary
|
||||
# Generates HTML table rows for every item in an array.
|
||||
# @liquid_description
|
||||
# The `tablerow` tag must be wrapped in HTML `<table>` and `</table>` tags.
|
||||
#
|
||||
# > Tip:
|
||||
# > Every `tablerow` loop has an associated [`tablerowloop` object](/docs/api/liquid/objects/tablerowloop) with information about the loop.
|
||||
# @liquid_syntax
|
||||
# {% tablerow variable in array %}
|
||||
# expression
|
||||
# {% endtablerow %}
|
||||
# @liquid_syntax_keyword variable The current item in the array.
|
||||
# @liquid_syntax_keyword array The array to iterate over.
|
||||
# @liquid_syntax_keyword expression The expression to render.
|
||||
# @liquid_optional_param cols [number] The number of columns that the table should have.
|
||||
# @liquid_optional_param limit [number] The number of iterations to perform.
|
||||
# @liquid_optional_param offset [number] The 1-based index to start iterating at.
|
||||
# @liquid_optional_param range [untyped] A custom numeric range to iterate over.
|
||||
class TableRow < Block
|
||||
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
|
||||
|
||||
attr_reader :variable_name, :collection_name, :attributes
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
if markup =~ Syntax
|
||||
@variable_name = $1
|
||||
@collection_name = Expression.parse($2)
|
||||
@attributes = {}
|
||||
@variable_name = Regexp.last_match(1)
|
||||
@collection_name = parse_expression(Regexp.last_match(2))
|
||||
@attributes = {}
|
||||
markup.scan(TagAttributes) do |key, value|
|
||||
@attributes[key] = Expression.parse(value)
|
||||
@attributes[key] = parse_expression(value)
|
||||
end
|
||||
else
|
||||
raise SyntaxError.new(options[:locale].t("errors.syntax.table_row".freeze))
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.table_row")
|
||||
end
|
||||
end
|
||||
|
||||
def render(context)
|
||||
collection = context.evaluate(@collection_name) or return ''.freeze
|
||||
def render_to_output_buffer(context, output)
|
||||
(collection = context.evaluate(@collection_name)) || (return '')
|
||||
|
||||
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
|
||||
from = @attributes.key?('offset') ? to_integer(context.evaluate(@attributes['offset'])) : 0
|
||||
to = @attributes.key?('limit') ? from + to_integer(context.evaluate(@attributes['limit'])) : nil
|
||||
|
||||
collection = Utils.slice_collection(collection, from, to)
|
||||
length = collection.length
|
||||
|
||||
length = collection.length
|
||||
cols = @attributes.key?('cols') ? to_integer(context.evaluate(@attributes['cols'])) : length
|
||||
|
||||
cols = context.evaluate(@attributes['cols'.freeze]).to_i
|
||||
|
||||
row = 1
|
||||
col = 0
|
||||
|
||||
result = "<tr class=\"row1\">\n"
|
||||
output << "<tr class=\"row1\">\n"
|
||||
context.stack do
|
||||
tablerowloop = Liquid::TablerowloopDrop.new(length, cols)
|
||||
context['tablerowloop'] = tablerowloop
|
||||
|
||||
collection.each_with_index do |item, index|
|
||||
collection.each do |item|
|
||||
context[@variable_name] = item
|
||||
context['tablerowloop'.freeze] = {
|
||||
'length'.freeze => length,
|
||||
'index'.freeze => index + 1,
|
||||
'index0'.freeze => index,
|
||||
'col'.freeze => col + 1,
|
||||
'col0'.freeze => col,
|
||||
'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)
|
||||
}
|
||||
|
||||
output << "<td class=\"col#{tablerowloop.col}\">"
|
||||
super
|
||||
output << '</td>'
|
||||
|
||||
col += 1
|
||||
|
||||
result << "<td class=\"col#{col}\">" << super << '</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
|
||||
output << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">"
|
||||
end
|
||||
|
||||
tablerowloop.send(:increment!)
|
||||
end
|
||||
end
|
||||
result << "</tr>\n"
|
||||
result
|
||||
|
||||
output << "</tr>\n"
|
||||
output
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
super + @node.attributes.values + [@node.collection_name]
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def to_integer(value)
|
||||
value.to_i
|
||||
rescue NoMethodError
|
||||
raise Liquid::ArgumentError, "invalid integer"
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('tablerow'.freeze, TableRow)
|
||||
Template.register_tag('tablerow', TableRow)
|
||||
end
|
||||
|
@ -1,31 +1,49 @@
|
||||
require File.dirname(__FILE__) + '/if'
|
||||
# frozen_string_literal: true
|
||||
|
||||
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 %}
|
||||
#
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category conditional
|
||||
# @liquid_name unless
|
||||
# @liquid_summary
|
||||
# Renders an expression unless a specific condition is `true`.
|
||||
# @liquid_description
|
||||
# > Tip:
|
||||
# > Similar to the [`if` tag](/docs/api/liquid/tags/if), you can use `elsif` to add more conditions to an `unless` tag.
|
||||
# @liquid_syntax
|
||||
# {% unless condition %}
|
||||
# expression
|
||||
# {% endunless %}
|
||||
# @liquid_syntax_keyword condition The condition to evaluate.
|
||||
# @liquid_syntax_keyword expression The expression to render unless the condition is met.
|
||||
class Unless < If
|
||||
def render(context)
|
||||
context.stack do
|
||||
def render_to_output_buffer(context, output)
|
||||
# First condition is interpreted backwards ( if not )
|
||||
first_block = @blocks.first
|
||||
result = Liquid::Utils.to_liquid_value(
|
||||
first_block.evaluate(context),
|
||||
)
|
||||
|
||||
# First condition is interpreted backwards ( if not )
|
||||
first_block = @blocks.first
|
||||
unless first_block.evaluate(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 block.attachment.render(context)
|
||||
end
|
||||
end
|
||||
|
||||
''.freeze
|
||||
unless result
|
||||
return first_block.attachment.render_to_output_buffer(context, output)
|
||||
end
|
||||
|
||||
# After the first condition unless works just like if
|
||||
@blocks[1..-1].each do |block|
|
||||
result = Liquid::Utils.to_liquid_value(
|
||||
block.evaluate(context),
|
||||
)
|
||||
|
||||
if result
|
||||
return block.attachment.render_to_output_buffer(context, output)
|
||||
end
|
||||
end
|
||||
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('unless'.freeze, Unless)
|
||||
Template.register_tag('unless', Unless)
|
||||
end
|
||||
|
@ -1,5 +1,6 @@
|
||||
module Liquid
|
||||
# frozen_string_literal: true
|
||||
|
||||
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 +15,19 @@ module Liquid
|
||||
# template.render('user_name' => 'bob')
|
||||
#
|
||||
class Template
|
||||
DEFAULT_OPTIONS = {
|
||||
:locale => I18n.new
|
||||
}
|
||||
|
||||
attr_accessor :root, :resource_limits
|
||||
@@file_system = BlankFileSystem.new
|
||||
attr_accessor :root, :name
|
||||
attr_reader :resource_limits, :warnings
|
||||
|
||||
class TagRegistry
|
||||
include Enumerable
|
||||
|
||||
def initialize
|
||||
@tags = {}
|
||||
@cache = {}
|
||||
end
|
||||
|
||||
def [](tag_name)
|
||||
return nil unless @tags.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 }
|
||||
@ -44,10 +43,14 @@ module Liquid
|
||||
@cache.delete(tag_name)
|
||||
end
|
||||
|
||||
def each(&block)
|
||||
@tags.each(&block)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def lookup_class(name)
|
||||
name.split("::").reject(&:empty?).reduce(Object) { |scope, const| scope.const_get(const) }
|
||||
Object.const_get(name)
|
||||
end
|
||||
end
|
||||
|
||||
@ -58,77 +61,57 @@ module Liquid
|
||||
# :lax acts like liquid 2.5 and silently ignores malformed tags in most cases.
|
||||
# :warn is the default and will give deprecation warnings when invalid syntax is used.
|
||||
# :strict will enforce correct syntax.
|
||||
attr_writer :error_mode
|
||||
attr_accessor :error_mode
|
||||
Template.error_mode = :lax
|
||||
|
||||
# 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
|
||||
attr_accessor :default_exception_renderer
|
||||
Template.default_exception_renderer = lambda do |exception|
|
||||
exception
|
||||
end
|
||||
|
||||
def file_system=(obj)
|
||||
@@file_system = obj
|
||||
end
|
||||
attr_accessor :file_system
|
||||
Template.file_system = BlankFileSystem.new
|
||||
|
||||
attr_accessor :tags
|
||||
Template.tags = TagRegistry.new
|
||||
private :tags=
|
||||
|
||||
def register_tag(name, klass)
|
||||
tags[name.to_s] = klass
|
||||
end
|
||||
|
||||
def tags
|
||||
@tags ||= TagRegistry.new
|
||||
end
|
||||
|
||||
def error_mode
|
||||
@error_mode || :lax
|
||||
end
|
||||
|
||||
def taint_mode
|
||||
@taint_mode || :lax
|
||||
end
|
||||
|
||||
# Pass a module with filter methods which should be available
|
||||
# to all liquid views. Good for registering the standard library
|
||||
def register_filter(mod)
|
||||
Strainer.global_filter(mod)
|
||||
StrainerFactory.add_global_filter(mod)
|
||||
end
|
||||
|
||||
def default_resource_limits
|
||||
@default_resource_limits ||= {}
|
||||
end
|
||||
attr_accessor :default_resource_limits
|
||||
Template.default_resource_limits = {}
|
||||
private :default_resource_limits=
|
||||
|
||||
# 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)
|
||||
new.parse(source, options)
|
||||
end
|
||||
end
|
||||
|
||||
def initialize
|
||||
@resource_limits = self.class.default_resource_limits.dup
|
||||
@rethrow_errors = false
|
||||
@resource_limits = ResourceLimits.new(Template.default_resource_limits)
|
||||
end
|
||||
|
||||
# Parse source code.
|
||||
# Returns self for easy chaining
|
||||
def parse(source, options = {})
|
||||
@options = options
|
||||
@profiling = options[:profile]
|
||||
@line_numbers = options[:line_numbers] || @profiling
|
||||
@root = Document.parse(tokenize(source), DEFAULT_OPTIONS.merge(options))
|
||||
@warnings = nil
|
||||
parse_context = configure_options(options)
|
||||
tokenizer = parse_context.new_tokenizer(source, start_line_number: @line_numbers && 1)
|
||||
@root = Document.parse(tokenizer, parse_context)
|
||||
self
|
||||
end
|
||||
|
||||
def warnings
|
||||
return [] unless @root
|
||||
@warnings ||= @root.warnings
|
||||
end
|
||||
|
||||
def registers
|
||||
@registers ||= {}
|
||||
end
|
||||
@ -160,19 +143,19 @@ module Liquid
|
||||
# filters and tags and might be useful to integrate liquid more with its host application
|
||||
#
|
||||
def render(*args)
|
||||
return ''.freeze if @root.nil?
|
||||
return '' if @root.nil?
|
||||
|
||||
context = case args.first
|
||||
when Liquid::Context
|
||||
c = args.shift
|
||||
|
||||
if @rethrow_errors
|
||||
c.exception_handler = ->(e) { true }
|
||||
c.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA
|
||||
end
|
||||
|
||||
c
|
||||
when Liquid::Drop
|
||||
drop = args.shift
|
||||
drop = args.shift
|
||||
drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
|
||||
when Hash
|
||||
Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
|
||||
@ -182,34 +165,35 @@ module Liquid
|
||||
raise ArgumentError, "Expected Hash or Liquid::Context as parameter"
|
||||
end
|
||||
|
||||
output = nil
|
||||
|
||||
case args.last
|
||||
when Hash
|
||||
options = args.pop
|
||||
output = options[:output] if options[:output]
|
||||
static_registers = context.registers.static
|
||||
|
||||
if options[:registers].is_a?(Hash)
|
||||
self.registers.merge!(options[:registers])
|
||||
options[:registers]&.each do |key, register|
|
||||
static_registers[key] = register
|
||||
end
|
||||
|
||||
if options[:filters]
|
||||
context.add_filters(options[:filters])
|
||||
end
|
||||
|
||||
if options[:exception_handler]
|
||||
context.exception_handler = options[:exception_handler]
|
||||
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
|
||||
|
||||
if @profiling && context.profiler.nil?
|
||||
@profiler = context.profiler = Liquid::Profiler.new
|
||||
end
|
||||
|
||||
context.template_name ||= name
|
||||
|
||||
begin
|
||||
# render the nodelist.
|
||||
# for performance reasons we get an array back here. join will make a string out of it.
|
||||
result = with_profiling do
|
||||
@root.render(context)
|
||||
end
|
||||
result.respond_to?(:join) ? result.join : result
|
||||
@root.render_to_output_buffer(context, output || +'')
|
||||
rescue Liquid::MemoryError => e
|
||||
context.handle_error(e)
|
||||
ensure
|
||||
@ -222,45 +206,31 @@ module Liquid
|
||||
render(*args)
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
render(context, output: output)
|
||||
end
|
||||
|
||||
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?
|
||||
def configure_options(options)
|
||||
if (profiling = options[:profile])
|
||||
raise "Profiler not loaded, require 'liquid/profiler' first" unless defined?(Liquid::Profiler)
|
||||
end
|
||||
|
||||
tokens = calculate_line_numbers(source.split(TemplateParser))
|
||||
|
||||
# removes the rogue empty element at the beginning of the array
|
||||
tokens.shift if tokens[0] and tokens[0].empty?
|
||||
|
||||
tokens
|
||||
@options = options
|
||||
@profiling = profiling
|
||||
@line_numbers = options[:line_numbers] || @profiling
|
||||
parse_context = options.is_a?(ParseContext) ? options : ParseContext.new(options)
|
||||
@warnings = parse_context.warnings
|
||||
parse_context
|
||||
end
|
||||
|
||||
def calculate_line_numbers(raw_tokens)
|
||||
return raw_tokens unless @line_numbers
|
||||
|
||||
current_line = 1
|
||||
raw_tokens.map do |token|
|
||||
Token.new(token, current_line).tap do
|
||||
current_line += token.count("\n")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def with_profiling
|
||||
if @profiling && !@options[:included]
|
||||
@profiler = Profiler.new
|
||||
@profiler.start
|
||||
|
||||
begin
|
||||
yield
|
||||
ensure
|
||||
@profiler.stop
|
||||
end
|
||||
else
|
||||
yield
|
||||
end
|
||||
def apply_options_to_context(context, options)
|
||||
context.add_filters(options[:filters]) if options[:filters]
|
||||
context.global_filter = options[:global_filter] if options[:global_filter]
|
||||
context.exception_renderer = options[:exception_renderer] if options[:exception_renderer]
|
||||
context.strict_variables = options[:strict_variables] if options[:strict_variables]
|
||||
context.strict_filters = options[:strict_filters] if options[:strict_filters]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
9
lib/liquid/template_factory.rb
Normal file
9
lib/liquid/template_factory.rb
Normal file
@ -0,0 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class TemplateFactory
|
||||
def for(_template_name)
|
||||
Liquid::Template.new
|
||||
end
|
||||
end
|
||||
end
|
@ -1,18 +0,0 @@
|
||||
module Liquid
|
||||
class Token < String
|
||||
attr_reader :line_number
|
||||
|
||||
def initialize(content, line_number)
|
||||
super(content)
|
||||
@line_number = line_number
|
||||
end
|
||||
|
||||
def raw
|
||||
"<raw>"
|
||||
end
|
||||
|
||||
def child(string)
|
||||
Token.new(string, @line_number)
|
||||
end
|
||||
end
|
||||
end
|
45
lib/liquid/tokenizer.rb
Normal file
45
lib/liquid/tokenizer.rb
Normal file
@ -0,0 +1,45 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Tokenizer
|
||||
attr_reader :line_number, :for_liquid_tag
|
||||
|
||||
def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false)
|
||||
@source = source.to_s.to_str
|
||||
@line_number = line_number || (line_numbers ? 1 : nil)
|
||||
@for_liquid_tag = for_liquid_tag
|
||||
@offset = 0
|
||||
@tokens = tokenize
|
||||
end
|
||||
|
||||
def shift
|
||||
token = @tokens[@offset]
|
||||
return nil unless token
|
||||
|
||||
@offset += 1
|
||||
|
||||
if @line_number
|
||||
@line_number += @for_liquid_tag ? 1 : token.count("\n")
|
||||
end
|
||||
|
||||
token
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def tokenize
|
||||
return [] if @source.empty?
|
||||
|
||||
return @source.split("\n") if @for_liquid_tag
|
||||
|
||||
tokens = @source.split(TemplateParser)
|
||||
|
||||
# removes the rogue empty element at the beginning of the array
|
||||
if tokens[0]&.empty?
|
||||
@offset += 1
|
||||
end
|
||||
|
||||
tokens
|
||||
end
|
||||
end
|
||||
end
|
8
lib/liquid/usage.rb
Normal file
8
lib/liquid/usage.rb
Normal file
@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
module Usage
|
||||
def self.increment(name)
|
||||
end
|
||||
end
|
||||
end
|
@ -1,27 +1,26 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
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
|
||||
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 +34,60 @@ 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(obj.to_s)
|
||||
when Numeric
|
||||
obj
|
||||
when String
|
||||
/\A-?\d+\.\d+\z/.match?(obj.strip) ? BigDecimal(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', 'today'
|
||||
Time.now
|
||||
when /\A\d+\z/, Integer
|
||||
Time.at(obj.to_i)
|
||||
when String
|
||||
Time.parse(obj)
|
||||
end
|
||||
rescue ::ArgumentError
|
||||
nil
|
||||
end
|
||||
|
||||
def self.to_liquid_value(obj)
|
||||
# Enable "obj" to represent itself as a primitive value like integer, string, or boolean
|
||||
return obj.to_liquid_value if obj.respond_to?(:to_liquid_value)
|
||||
|
||||
# Otherwise return the object itself
|
||||
obj
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1,5 +1,6 @@
|
||||
module Liquid
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# Holds variables. Variables are only loaded "just in time"
|
||||
# and are not evaluated as part of the render stage
|
||||
#
|
||||
@ -11,17 +12,25 @@ module Liquid
|
||||
# {{ user | link }}
|
||||
#
|
||||
class Variable
|
||||
FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
|
||||
attr_accessor :filters, :name, :warnings
|
||||
attr_accessor :line_number
|
||||
FilterMarkupRegex = /#{FilterSeparator}\s*(.*)/om
|
||||
FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
|
||||
FilterArgsRegex = /(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o
|
||||
JustTagAttributes = /\A#{TagAttributes}\z/o
|
||||
MarkupWithQuotedFragment = /(#{QuotedFragment})(.*)/om
|
||||
|
||||
attr_accessor :filters, :name, :line_number
|
||||
attr_reader :parse_context
|
||||
alias_method :options, :parse_context
|
||||
|
||||
include ParserSwitching
|
||||
|
||||
def initialize(markup, options = {})
|
||||
@markup = markup
|
||||
@name = nil
|
||||
@options = options || {}
|
||||
def initialize(markup, parse_context)
|
||||
@markup = markup
|
||||
@name = nil
|
||||
@parse_context = parse_context
|
||||
@line_number = parse_context.line_number
|
||||
|
||||
parse_with_selected_parser(markup)
|
||||
strict_parse_with_error_mode_fallback(markup)
|
||||
end
|
||||
|
||||
def raw
|
||||
@ -34,19 +43,18 @@ module Liquid
|
||||
|
||||
def lax_parse(markup)
|
||||
@filters = []
|
||||
if 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|
|
||||
if 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
|
||||
return unless markup =~ MarkupWithQuotedFragment
|
||||
|
||||
name_markup = Regexp.last_match(1)
|
||||
filter_markup = Regexp.last_match(2)
|
||||
@name = parse_context.parse_expression(name_markup)
|
||||
if filter_markup =~ FilterMarkupRegex
|
||||
filters = Regexp.last_match(1).scan(FilterParser)
|
||||
filters.each do |f|
|
||||
next unless f =~ /\w+/
|
||||
filtername = Regexp.last_match(0)
|
||||
filterargs = f.scan(FilterArgsRegex).flatten
|
||||
@filters << parse_filter_expressions(filtername, filterargs)
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -55,7 +63,9 @@ module Liquid
|
||||
@filters = []
|
||||
p = Parser.new(markup)
|
||||
|
||||
@name = Expression.parse(p.expression)
|
||||
return if p.look(:end_of_string)
|
||||
|
||||
@name = parse_context.parse_expression(p.expression)
|
||||
while p.consume?(:pipe)
|
||||
filtername = p.consume(:id)
|
||||
filterargs = p.consume?(:colon) ? parse_filterargs(p) : []
|
||||
@ -68,38 +78,62 @@ module Liquid
|
||||
# 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)
|
||||
@filters.inject(context.evaluate(@name)) do |output, (filter_name, filter_args, filter_kwargs)|
|
||||
obj = context.evaluate(@name)
|
||||
|
||||
@filters.each do |filter_name, filter_args, filter_kwargs|
|
||||
filter_args = evaluate_filter_expressions(context, filter_args, filter_kwargs)
|
||||
output = context.invoke(filter_name, output, *filter_args)
|
||||
end.tap{ |obj| taint_check(obj) }
|
||||
obj = context.invoke(filter_name, obj, *filter_args)
|
||||
end
|
||||
|
||||
context.apply_global_filter(obj)
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
obj = render(context)
|
||||
|
||||
if obj.is_a?(Array)
|
||||
output << obj.join
|
||||
elsif obj.nil?
|
||||
else
|
||||
output << obj.to_s
|
||||
end
|
||||
|
||||
output
|
||||
end
|
||||
|
||||
def disabled?(_context)
|
||||
false
|
||||
end
|
||||
|
||||
def disabled_tags
|
||||
[]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_filter_expressions(filter_name, unparsed_args)
|
||||
filter_args = []
|
||||
keyword_args = {}
|
||||
filter_args = []
|
||||
keyword_args = nil
|
||||
unparsed_args.each do |a|
|
||||
if matches = a.match(/\A#{TagAttributes}\z/o)
|
||||
keyword_args[matches[1]] = Expression.parse(matches[2])
|
||||
if (matches = a.match(JustTagAttributes))
|
||||
keyword_args ||= {}
|
||||
keyword_args[matches[1]] = parse_context.parse_expression(matches[2])
|
||||
else
|
||||
filter_args << Expression.parse(a)
|
||||
filter_args << parse_context.parse_expression(a)
|
||||
end
|
||||
end
|
||||
result = [filter_name, filter_args]
|
||||
result << keyword_args unless keyword_args.empty?
|
||||
result << keyword_args if keyword_args
|
||||
result
|
||||
end
|
||||
|
||||
def evaluate_filter_expressions(context, filter_args, filter_kwargs)
|
||||
parsed_args = filter_args.map{ |expr| context.evaluate(expr) }
|
||||
parsed_args = filter_args.map { |expr| context.evaluate(expr) }
|
||||
if filter_kwargs
|
||||
parsed_kwargs = {}
|
||||
filter_kwargs.each do |key, expr|
|
||||
@ -110,17 +144,9 @@ module Liquid
|
||||
parsed_args
|
||||
end
|
||||
|
||||
def taint_check(obj)
|
||||
if obj.tainted?
|
||||
@markup =~ QuotedFragment
|
||||
name = Regexp.last_match(0)
|
||||
case Template.taint_mode
|
||||
when :warn
|
||||
@warnings ||= []
|
||||
@warnings << "variable '#{name}' is tainted and was not escaped"
|
||||
when :error
|
||||
raise TaintedError, "Error - variable '#{name}' is tainted and was not escaped"
|
||||
end
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
[@node.name] + @node.filters.flatten
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1,7 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class VariableLookup
|
||||
SQUARE_BRACKETED = /\A\[(.*)\]\z/m
|
||||
COMMAND_METHODS = ['size'.freeze, 'first'.freeze, 'last'.freeze]
|
||||
COMMAND_METHODS = ['size', 'first', 'last'].freeze
|
||||
|
||||
attr_reader :name, :lookups
|
||||
|
||||
def self.parse(markup)
|
||||
new(markup)
|
||||
@ -11,51 +14,60 @@ module Liquid
|
||||
lookups = markup.scan(VariableParser)
|
||||
|
||||
name = lookups.shift
|
||||
if name =~ SQUARE_BRACKETED
|
||||
name = Expression.parse($1)
|
||||
if name&.start_with?('[') && name&.end_with?(']')
|
||||
name = Expression.parse(name[1..-2])
|
||||
end
|
||||
@name = name
|
||||
|
||||
@lookups = lookups
|
||||
@lookups = lookups
|
||||
@command_flags = 0
|
||||
|
||||
@lookups.each_index do |i|
|
||||
lookup = lookups[i]
|
||||
if lookup =~ SQUARE_BRACKETED
|
||||
lookups[i] = Expression.parse($1)
|
||||
if lookup&.start_with?('[') && lookup&.end_with?(']')
|
||||
lookups[i] = Expression.parse(lookup[1..-2])
|
||||
elsif COMMAND_METHODS.include?(lookup)
|
||||
@command_flags |= 1 << i
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def lookup_command?(lookup_index)
|
||||
@command_flags & (1 << lookup_index) != 0
|
||||
end
|
||||
|
||||
def evaluate(context)
|
||||
name = context.evaluate(@name)
|
||||
name = context.evaluate(@name)
|
||||
object = context.find_variable(name)
|
||||
|
||||
@lookups.each_index do |i|
|
||||
key = context.evaluate(@lookups[i])
|
||||
|
||||
# Cast "key" to its liquid value to enable it to act as a primitive value
|
||||
key = Liquid::Utils.to_liquid_value(key)
|
||||
|
||||
# If object is a hash- or array-like object we look for the
|
||||
# presence of the key and if its available we return it
|
||||
if object.respond_to?(:[]) &&
|
||||
((object.respond_to?(:has_key?) && object.has_key?(key)) ||
|
||||
(object.respond_to?(:fetch) && key.is_a?(Integer)))
|
||||
((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)
|
||||
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)
|
||||
elsif lookup_command?(i) && 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
|
||||
# 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
|
||||
return nil unless context.strict_variables
|
||||
raise Liquid::UndefinedVariable, "undefined variable #{key}"
|
||||
end
|
||||
|
||||
# If we are dealing with a drop here we have to
|
||||
@ -66,7 +78,7 @@ module Liquid
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
self.class == other.class && self.state == other.state
|
||||
self.class == other.class && state == other.state
|
||||
end
|
||||
|
||||
protected
|
||||
@ -74,5 +86,11 @@ module Liquid
|
||||
def state
|
||||
[@name, @lookups, @command_flags]
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
@node.lookups
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1,4 +1,6 @@
|
||||
# encoding: utf-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
VERSION = "3.0.0"
|
||||
VERSION = "5.4.0"
|
||||
end
|
||||
|
@ -1,6 +1,8 @@
|
||||
# encoding: utf-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
lib = File.expand_path('../lib/', __FILE__)
|
||||
$:.unshift lib unless $:.include?(lib)
|
||||
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
||||
|
||||
require "liquid/version"
|
||||
|
||||
@ -9,21 +11,23 @@ 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_ruby_version = ">= 2.7.0"
|
||||
s.required_rubygems_version = ">= 1.3.7"
|
||||
|
||||
s.test_files = Dir.glob("{test}/**/*")
|
||||
s.files = Dir.glob("{lib}/**/*") + %w(MIT-LICENSE README.md)
|
||||
s.metadata['allowed_push_host'] = 'https://rubygems.org'
|
||||
|
||||
s.extra_rdoc_files = ["History.md", "README.md"]
|
||||
s.files = Dir.glob("{lib}/**/*") + %w(LICENSE README.md)
|
||||
|
||||
s.extra_rdoc_files = ["History.md", "README.md"]
|
||||
|
||||
s.require_path = "lib"
|
||||
|
||||
s.add_development_dependency 'rake'
|
||||
s.add_development_dependency 'minitest'
|
||||
s.add_development_dependency('rake', '~> 13.0')
|
||||
s.add_development_dependency('minitest')
|
||||
end
|
||||
|
@ -1,11 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'benchmark/ips'
|
||||
require File.dirname(__FILE__) + '/theme_runner'
|
||||
require_relative 'theme_runner'
|
||||
|
||||
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
|
||||
profiler = ThemeRunner.new
|
||||
|
||||
Benchmark.ips do |x|
|
||||
x.time = 60
|
||||
x.time = 10
|
||||
x.warmup = 5
|
||||
|
||||
puts
|
||||
@ -13,5 +15,6 @@ Benchmark.ips do |x|
|
||||
puts
|
||||
|
||||
x.report("parse:") { profiler.compile }
|
||||
x.report("parse & run:") { profiler.run }
|
||||
x.report("render:") { profiler.render }
|
||||
x.report("parse & render:") { profiler.run }
|
||||
end
|
||||
|
63
performance/memory_profile.rb
Normal file
63
performance/memory_profile.rb
Normal file
@ -0,0 +1,63 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'benchmark/ips'
|
||||
require 'memory_profiler'
|
||||
require 'terminal-table'
|
||||
require_relative 'theme_runner'
|
||||
|
||||
class Profiler
|
||||
LOG_LABEL = "Profiling: ".rjust(14).freeze
|
||||
REPORTS_DIR = File.expand_path('.memprof', __dir__).freeze
|
||||
|
||||
def self.run
|
||||
puts
|
||||
yield new
|
||||
end
|
||||
|
||||
def initialize
|
||||
@allocated = []
|
||||
@retained = []
|
||||
@headings = []
|
||||
end
|
||||
|
||||
def profile(phase, &block)
|
||||
print(LOG_LABEL)
|
||||
print("#{phase}.. ".ljust(10))
|
||||
report = MemoryProfiler.report(&block)
|
||||
puts 'Done.'
|
||||
@headings << phase.capitalize
|
||||
@allocated << "#{report.scale_bytes(report.total_allocated_memsize)} (#{report.total_allocated} objects)"
|
||||
@retained << "#{report.scale_bytes(report.total_retained_memsize)} (#{report.total_retained} objects)"
|
||||
|
||||
return if ENV['CI']
|
||||
|
||||
require 'fileutils'
|
||||
report_file = File.join(REPORTS_DIR, "#{sanitize(phase)}.txt")
|
||||
FileUtils.mkdir_p(REPORTS_DIR)
|
||||
report.pretty_print(to_file: report_file, scale_bytes: true)
|
||||
end
|
||||
|
||||
def tabulate
|
||||
table = Terminal::Table.new(headings: @headings.unshift('Phase')) do |t|
|
||||
t << @allocated.unshift('Total allocated')
|
||||
t << @retained.unshift('Total retained')
|
||||
end
|
||||
|
||||
puts
|
||||
puts table
|
||||
puts "\nDetailed report(s) saved to #{REPORTS_DIR}/" unless ENV['CI']
|
||||
end
|
||||
|
||||
def sanitize(string)
|
||||
string.downcase.gsub(/[\W]/, '-').squeeze('-')
|
||||
end
|
||||
end
|
||||
|
||||
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
|
||||
|
||||
runner = ThemeRunner.new
|
||||
Profiler.run do |x|
|
||||
x.profile('parse') { runner.compile }
|
||||
x.profile('render') { runner.render }
|
||||
x.tabulate
|
||||
end
|
@ -1,19 +1,21 @@
|
||||
require 'stackprof' rescue fail("install stackprof extension/gem")
|
||||
require File.dirname(__FILE__) + '/theme_runner'
|
||||
# frozen_string_literal: true
|
||||
|
||||
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
|
||||
200.times do
|
||||
profiler.run
|
||||
end
|
||||
end
|
||||
|
||||
if profile_type == :cpu && graph_filename = ENV['GRAPH_FILENAME']
|
||||
if profile_type == :cpu && (graph_filename = ENV['GRAPH_FILENAME'])
|
||||
File.open(graph_filename, 'w') do |f|
|
||||
StackProf::Report.new(results).print_graphviz(nil, f)
|
||||
end
|
||||
|
@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class CommentForm < Liquid::Block
|
||||
Syntax = /(#{Liquid::VariableSignature}+)/
|
||||
|
||||
@ -5,14 +7,14 @@ class CommentForm < Liquid::Block
|
||||
super
|
||||
|
||||
if markup =~ Syntax
|
||||
@variable_name = $1
|
||||
@attributes = {}
|
||||
@variable_name = Regexp.last_match(1)
|
||||
@attributes = {}
|
||||
else
|
||||
raise SyntaxError.new("Syntax Error in 'comment_form' - Valid syntax: comment_form [article]")
|
||||
raise SyntaxError, "Syntax Error in 'comment_form' - Valid syntax: comment_form [article]"
|
||||
end
|
||||
end
|
||||
|
||||
def render(context)
|
||||
def render_to_output_buffer(context, output)
|
||||
article = context[@variable_name]
|
||||
|
||||
context.stack do
|
||||
@ -20,14 +22,16 @@ class CommentForm < Liquid::Block
|
||||
'posted_successfully?' => context.registers[:posted_successfully],
|
||||
'errors' => context['comment.errors'],
|
||||
'author' => context['comment.author'],
|
||||
'email' => context['comment.email'],
|
||||
'body' => context['comment.body']
|
||||
'email' => context['comment.email'],
|
||||
'body' => context['comment.body'],
|
||||
}
|
||||
wrap_in_form(article, render_all(@nodelist, context))
|
||||
|
||||
output << wrap_in_form(article, render_all(@nodelist, context, output))
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
|
@ -1,11 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'yaml'
|
||||
|
||||
module Database
|
||||
DATABASE_FILE_PATH = "#{__dir__}/vision.database.yml"
|
||||
|
||||
# Load the standard vision toolkit database and re-arrage it to be simply exportable
|
||||
# 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 =
|
||||
if YAML.respond_to?(:unsafe_load_file) # Only Psych 4+ can use unsafe_load_file
|
||||
# unsafe_load_file is needed for YAML references
|
||||
YAML.unsafe_load_file(DATABASE_FILE_PATH)
|
||||
else
|
||||
YAML.load_file(DATABASE_FILE_PATH)
|
||||
end
|
||||
|
||||
# From vision source
|
||||
db['products'].each do |product|
|
||||
@ -16,9 +26,10 @@ module Database
|
||||
end
|
||||
|
||||
# key the tables by handles, as this is how liquid expects it.
|
||||
db = db.inject({}) do |assigns, (key, values)|
|
||||
assigns[key] = values.inject({}) { |h, v| h[v['handle']] = v; h; }
|
||||
assigns
|
||||
db = db.each_with_object({}) do |(key, values), assigns|
|
||||
assigns[key] = values.each_with_object({}) do |v, h|
|
||||
h[v['handle']] = v
|
||||
end
|
||||
end
|
||||
|
||||
# Some standard direct accessors so that the specialized templates
|
||||
@ -29,17 +40,12 @@ module Database
|
||||
db['article'] = db['blog']['articles'].first
|
||||
|
||||
db['cart'] = {
|
||||
'total_price' => db['line_items'].values.inject(0) { |sum, item| sum += item['line_price'] * item['quantity'] },
|
||||
'item_count' => db['line_items'].values.inject(0) { |sum, item| sum += item['quantity'] },
|
||||
'items' => db['line_items'].values
|
||||
'total_price' => db['line_items'].values.inject(0) { |sum, item| sum + item['line_price'] * item['quantity'] },
|
||||
'item_count' => db['line_items'].values.inject(0) { |sum, item| sum + item['quantity'] },
|
||||
'items' => db['line_items'].values,
|
||||
}
|
||||
|
||||
db
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if __FILE__ == $0
|
||||
p Database.tables['collections']['frontpage'].keys
|
||||
#p Database.tables['blog']['articles']
|
||||
end
|
||||
|
@ -1,9 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
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,19 +1,21 @@
|
||||
$:.unshift File.dirname(__FILE__) + '/../../lib'
|
||||
require File.dirname(__FILE__) + '/../../lib/liquid'
|
||||
# frozen_string_literal: true
|
||||
|
||||
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'
|
||||
$LOAD_PATH.unshift(__dir__ + '/../../lib')
|
||||
require_relative '../../lib/liquid'
|
||||
|
||||
Liquid::Template.register_tag 'paginate', Paginate
|
||||
Liquid::Template.register_tag 'form', CommentForm
|
||||
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_filter JsonFilter
|
||||
Liquid::Template.register_filter MoneyFilter
|
||||
Liquid::Template.register_filter WeightFilter
|
||||
Liquid::Template.register_filter ShopFilter
|
||||
Liquid::Template.register_filter TagFilter
|
||||
Liquid::Template.register_tag('paginate', Paginate)
|
||||
Liquid::Template.register_tag('form', CommentForm)
|
||||
|
||||
Liquid::Template.register_filter(JsonFilter)
|
||||
Liquid::Template.register_filter(MoneyFilter)
|
||||
Liquid::Template.register_filter(WeightFilter)
|
||||
Liquid::Template.register_filter(ShopFilter)
|
||||
Liquid::Template.register_filter(TagFilter)
|
||||
|
@ -1,13 +1,14 @@
|
||||
module MoneyFilter
|
||||
# frozen_string_literal: true
|
||||
|
||||
module MoneyFilter
|
||||
def money_with_currency(money)
|
||||
return '' if money.nil?
|
||||
sprintf("$ %.2f USD", money/100.0)
|
||||
format("$ %.2f USD", money / 100.0)
|
||||
end
|
||||
|
||||
def money(money)
|
||||
return '' if money.nil?
|
||||
sprintf("$ %.2f", money/100.0)
|
||||
format("$ %.2f", money / 100.0)
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -1,13 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Paginate < Liquid::Block
|
||||
Syntax = /(#{Liquid::QuotedFragment})\s*(by\s*(\d+))?/
|
||||
Syntax = /(#{Liquid::QuotedFragment})\s*(by\s*(\d+))?/
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
|
||||
if markup =~ Syntax
|
||||
@collection_name = $1
|
||||
@page_size = if $2
|
||||
$3.to_i
|
||||
@collection_name = Regexp.last_match(1)
|
||||
@page_size = if Regexp.last_match(2)
|
||||
Regexp.last_match(3).to_i
|
||||
else
|
||||
20
|
||||
end
|
||||
@ -17,48 +19,47 @@ class Paginate < Liquid::Block
|
||||
@attributes[key] = value
|
||||
end
|
||||
else
|
||||
raise SyntaxError.new("Syntax Error in tag 'paginate' - Valid syntax: paginate [collection] by number")
|
||||
raise SyntaxError, "Syntax Error in tag 'paginate' - Valid syntax: paginate [collection] by number"
|
||||
end
|
||||
end
|
||||
|
||||
def render(context)
|
||||
def render_to_output_buffer(context, output)
|
||||
@context = context
|
||||
|
||||
context.stack do
|
||||
current_page = context['current_page'].to_i
|
||||
current_page = context['current_page'].to_i
|
||||
|
||||
pagination = {
|
||||
'page_size' => @page_size,
|
||||
'current_page' => 5,
|
||||
'current_offset' => @page_size * 5
|
||||
'page_size' => @page_size,
|
||||
'current_page' => 5,
|
||||
'current_offset' => @page_size * 5,
|
||||
}
|
||||
|
||||
context['paginate'] = pagination
|
||||
|
||||
collection_size = context[@collection_name].size
|
||||
collection_size = context[@collection_name].size
|
||||
|
||||
raise ArgumentError.new("Cannot paginate array '#{@collection_name}'. Not found.") if collection_size.nil?
|
||||
raise ArgumentError, "Cannot paginate array '#{@collection_name}'. Not found." if collection_size.nil?
|
||||
|
||||
page_count = (collection_size.to_f / @page_size.to_f).to_f.ceil + 1
|
||||
|
||||
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) if 1 < current_page
|
||||
pagination['next'] = link('Next »', current_page + 1) if 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
|
||||
@ -78,11 +79,11 @@ class Paginate < Liquid::Block
|
||||
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,6 @@
|
||||
module ShopFilter
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ShopFilter
|
||||
def asset_url(input)
|
||||
"/files/1/[shop_id]/[shop_id]/assets/#{input}"
|
||||
end
|
||||
@ -16,21 +17,21 @@ 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)
|
||||
if vendor
|
||||
link_to vendor, url_for_vendor(vendor), vendor
|
||||
link_to(vendor, url_for_vendor(vendor), vendor)
|
||||
else
|
||||
'Unknown Vendor'
|
||||
end
|
||||
@ -38,7 +39,7 @@ module ShopFilter
|
||||
|
||||
def link_to_type(type)
|
||||
if type
|
||||
link_to type, url_for_type(type), type
|
||||
link_to(type, url_for_type(type), type)
|
||||
else
|
||||
'Unknown Vendor'
|
||||
end
|
||||
@ -53,36 +54,32 @@ module ShopFilter
|
||||
end
|
||||
|
||||
def product_img_url(url, style = 'small')
|
||||
|
||||
unless url =~ /\Aproducts\/([\w\-\_]+)\.(\w{2,4})/
|
||||
unless url =~ %r{\Aproducts/([\w\-\_]+)\.(\w{2,4})}
|
||||
raise ArgumentError, 'filter "size" can only be called on product images'
|
||||
end
|
||||
|
||||
case style
|
||||
when 'original'
|
||||
return '/files/shops/random_number/' + url
|
||||
'/files/shops/random_number/' + url
|
||||
when 'grande', 'large', 'medium', 'compact', 'small', 'thumb', 'icon'
|
||||
"/files/shops/random_number/products/#{$1}_#{style}.#{$2}"
|
||||
"/files/shops/random_number/products/#{Regexp.last_match(1)}_#{style}.#{Regexp.last_match(2)}"
|
||||
else
|
||||
raise ArgumentError, 'valid parameters for filter "size" are: original, grande, large, medium, compact, small, thumb and icon '
|
||||
end
|
||||
end
|
||||
|
||||
def default_pagination(paginate)
|
||||
|
||||
html = []
|
||||
html << %(<span class="prev">#{link_to(paginate['previous']['title'], paginate['previous']['url'])}</span>) if paginate['previous']
|
||||
|
||||
for part in paginate['parts']
|
||||
|
||||
if part['is_link']
|
||||
html << %(<span class="page">#{link_to(part['title'], part['url'])}</span>)
|
||||
paginate['parts'].each do |part|
|
||||
html << if part['is_link']
|
||||
%(<span class="page">#{link_to(part['title'], part['url'])}</span>)
|
||||
elsif part['title'].to_i == paginate['current_page'].to_i
|
||||
html << %(<span class="page current">#{part['title']}</span>)
|
||||
%(<span class="page current">#{part['title']}</span>)
|
||||
else
|
||||
html << %(<span class="deco">#{part['title']}</span>)
|
||||
%(<span class="deco">#{part['title']}</span>)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
html << %(<span class="next">#{link_to(paginate['next']['title'], paginate['next']['url'])}</span>) if paginate['next']
|
||||
@ -106,5 +103,4 @@ module ShopFilter
|
||||
result.gsub!(/\A-+/, '') if result[0] == '-'
|
||||
result
|
||||
end
|
||||
|
||||
end
|
||||
|
@ -1,10 +1,11 @@
|
||||
module TagFilter
|
||||
# frozen_string_literal: true
|
||||
|
||||
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
|
||||
@ -14,12 +15,11 @@ module TagFilter
|
||||
|
||||
def link_to_add_tag(label, tag)
|
||||
tags = (@context['current_tags'] + [tag]).uniq
|
||||
"<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tags.join("+")}\">#{label}</a>"
|
||||
"<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tags.join('+')}\">#{label}</a>"
|
||||
end
|
||||
|
||||
def link_to_remove_tag(label, tag)
|
||||
tags = (@context['current_tags'] - [tag]).uniq
|
||||
"<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tags.join("+")}\">#{label}</a>"
|
||||
"<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tags.join('+')}\">#{label}</a>"
|
||||
end
|
||||
|
||||
end
|
||||
|
@ -1,11 +1,11 @@
|
||||
module WeightFilter
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WeightFilter
|
||||
def weight(grams)
|
||||
sprintf("%.2f", grams / 1000)
|
||||
format("%.2f", grams / 1000)
|
||||
end
|
||||
|
||||
def weight_with_unit(grams)
|
||||
"#{weight(grams)} kg"
|
||||
end
|
||||
|
||||
end
|
||||
|
@ -1,78 +1,126 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# This profiler run simulates Shopify.
|
||||
# We are looking in the tests directory for liquid files and render them within the designated layout file.
|
||||
# We will also export a substantial database to liquid which the templates can render values of.
|
||||
# All this is to make the benchmark as non syntetic as possible. All templates and tests are lifted from
|
||||
# All this is to make the benchmark as non synthetic as possible. All templates and tests are lifted from
|
||||
# direct real-world usage and the profiler measures code that looks very similar to the way it looks in
|
||||
# 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
|
||||
|
||||
# Load all templates into memory, do this now so that
|
||||
# we don't profile IO.
|
||||
# Initialize a new liquid ThemeRunner instance
|
||||
# Will 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'
|
||||
|
||||
[File.read(test), (File.file?(theme_path) ? File.read(theme_path) : nil), test]
|
||||
{
|
||||
liquid: File.read(test),
|
||||
layout: (File.file?(theme_path) ? File.read(theme_path) : nil),
|
||||
template_name: test,
|
||||
}
|
||||
end.compact
|
||||
|
||||
compile_all_tests
|
||||
end
|
||||
|
||||
# `compile` will test just the compilation portion of liquid without any templates
|
||||
def compile
|
||||
# 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
|
||||
tmpl.parse(layout)
|
||||
@tests.each do |test_hash|
|
||||
Liquid::Template.new.parse(test_hash[:liquid])
|
||||
Liquid::Template.new.parse(test_hash[:layout])
|
||||
end
|
||||
end
|
||||
|
||||
def run
|
||||
# `run` is called to benchmark rendering and compiling at the same time
|
||||
def run
|
||||
each_test do |liquid, layout, assigns, page_template, template_name|
|
||||
compile_and_render(liquid, layout, assigns, page_template, template_name)
|
||||
end
|
||||
end
|
||||
|
||||
# `render` is called to benchmark just the render portion of liquid
|
||||
def render
|
||||
@compiled_tests.each do |test|
|
||||
tmpl = test[:tmpl]
|
||||
assigns = test[:assigns]
|
||||
layout = test[:layout]
|
||||
|
||||
if layout
|
||||
assigns['content_for_layout'] = tmpl.render!(assigns)
|
||||
layout.render!(assigns)
|
||||
else
|
||||
tmpl.render!(assigns)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def render_layout(template, layout, assigns)
|
||||
assigns['content_for_layout'] = template.render!(assigns)
|
||||
layout&.render!(assigns)
|
||||
end
|
||||
|
||||
def compile_and_render(template, layout, assigns, page_template, template_file)
|
||||
compiled_test = compile_test(template, layout, assigns, page_template, template_file)
|
||||
render_layout(compiled_test[:tmpl], compiled_test[:layout], compiled_test[:assigns])
|
||||
end
|
||||
|
||||
def compile_all_tests
|
||||
@compiled_tests = []
|
||||
each_test do |liquid, layout, assigns, page_template, template_name|
|
||||
@compiled_tests << compile_test(liquid, layout, assigns, page_template, template_name)
|
||||
end
|
||||
@compiled_tests
|
||||
end
|
||||
|
||||
def compile_test(template, layout, assigns, page_template, template_file)
|
||||
tmpl = init_template(page_template, template_file)
|
||||
parsed_template = tmpl.parse(template).dup
|
||||
|
||||
if layout
|
||||
parsed_layout = tmpl.parse(layout)
|
||||
{ tmpl: parsed_template, assigns: assigns, layout: parsed_layout }
|
||||
else
|
||||
{ tmpl: parsed_template, assigns: assigns }
|
||||
end
|
||||
end
|
||||
|
||||
# utility method with similar functionality needed in `compile_all_tests` and `run`
|
||||
def each_test
|
||||
# Dup assigns because will make some changes to them
|
||||
assigns = Database.tables.dup
|
||||
|
||||
@tests.each do |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)
|
||||
|
||||
@tests.each do |test_hash|
|
||||
# Compute page_template outside of profiler run, uninteresting to profiler
|
||||
page_template = File.basename(test_hash[:template_name], File.extname(test_hash[:template_name]))
|
||||
yield(test_hash[:liquid], test_hash[:layout], assigns, page_template, test_hash[:template_name])
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def compile_and_render(template, layout, assigns, page_template, template_file)
|
||||
tmpl = Liquid::Template.new
|
||||
tmpl.assigns['page_title'] = 'Page title'
|
||||
tmpl.assigns['template'] = page_template
|
||||
# set up a new Liquid::Template object for use in `compile_and_render` and `compile_test`
|
||||
def init_template(page_template, template_file)
|
||||
tmpl = Liquid::Template.new
|
||||
tmpl.assigns['page_title'] = 'Page title'
|
||||
tmpl.assigns['template'] = page_template
|
||||
tmpl.registers[:file_system] = ThemeRunner::FileSystem.new(File.dirname(template_file))
|
||||
|
||||
content_for_layout = tmpl.parse(template).render!(assigns)
|
||||
|
||||
if layout
|
||||
assigns['content_for_layout'] = content_for_layout
|
||||
tmpl.parse(layout).render!(assigns)
|
||||
else
|
||||
content_for_layout
|
||||
end
|
||||
tmpl
|
||||
end
|
||||
end
|
||||
|
@ -1,38 +1,117 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class AssignTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_assigned_variable
|
||||
assert_template_result('.foo.',
|
||||
'{% assign foo = values %}.{{ foo[0] }}.',
|
||||
'values' => %w{foo bar baz})
|
||||
def test_assign_with_hyphen_in_variable_name
|
||||
template_source = <<~END_TEMPLATE
|
||||
{% assign this-thing = 'Print this-thing' -%}
|
||||
{{ this-thing -}}
|
||||
END_TEMPLATE
|
||||
assert_template_result("Print this-thing", template_source)
|
||||
end
|
||||
|
||||
assert_template_result('.bar.',
|
||||
'{% assign foo = values %}.{{ foo[1] }}.',
|
||||
'values' => %w{foo bar baz})
|
||||
def test_assigned_variable
|
||||
assert_template_result(
|
||||
'.foo.',
|
||||
'{% assign foo = values %}.{{ foo[0] }}.',
|
||||
{ 'values' => %w(foo bar baz) },
|
||||
)
|
||||
|
||||
assert_template_result(
|
||||
'.bar.',
|
||||
'{% 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")
|
||||
assert_template_result(
|
||||
'.bar.',
|
||||
'{% 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")
|
||||
assert_match_syntax_error(/assign/, '{% assign foo not values %}.')
|
||||
end
|
||||
|
||||
def test_assign_uses_error_mode
|
||||
with_error_mode(:strict) do
|
||||
assert_raises(SyntaxError) do
|
||||
Template.parse("{% assign foo = ('X' | downcase) %}")
|
||||
end
|
||||
assert_match_syntax_error(
|
||||
"Expected dotdot but found pipe in ",
|
||||
"{% assign foo = ('X' | downcase) %}",
|
||||
error_mode: :strict,
|
||||
)
|
||||
assert_template_result("", "{% assign foo = ('X' | downcase) %}", error_mode: :lax)
|
||||
end
|
||||
|
||||
def test_expression_with_whitespace_in_square_brackets
|
||||
source = "{% assign r = a[ 'b' ] %}{{ r }}"
|
||||
assert_template_result('result', source, { 'a' => { 'b' => 'result' } })
|
||||
end
|
||||
|
||||
def test_assign_score_exceeding_resource_limit
|
||||
t = Template.parse("{% assign foo = 42 %}{% assign bar = 23 %}")
|
||||
t.resource_limits.assign_score_limit = 1
|
||||
assert_equal("Liquid error: Memory limits exceeded", t.render)
|
||||
assert(t.resource_limits.reached?)
|
||||
|
||||
t.resource_limits.assign_score_limit = 2
|
||||
assert_equal("", t.render!)
|
||||
refute_nil(t.resource_limits.assign_score)
|
||||
end
|
||||
|
||||
def test_assign_score_exceeding_limit_from_composite_object
|
||||
t = Template.parse("{% assign foo = 'aaaa' | reverse %}")
|
||||
|
||||
t.resource_limits.assign_score_limit = 3
|
||||
assert_equal("Liquid error: Memory limits exceeded", t.render)
|
||||
assert(t.resource_limits.reached?)
|
||||
|
||||
t.resource_limits.assign_score_limit = 5
|
||||
assert_equal("", t.render!)
|
||||
end
|
||||
|
||||
def test_assign_score_of_int
|
||||
assert_equal(1, assign_score_of(123))
|
||||
end
|
||||
|
||||
def test_assign_score_of_string_counts_bytes
|
||||
assert_equal(3, assign_score_of('123'))
|
||||
assert_equal(5, assign_score_of('12345'))
|
||||
assert_equal(9, assign_score_of('すごい'))
|
||||
end
|
||||
|
||||
def test_assign_score_of_array
|
||||
assert_equal(1, assign_score_of([]))
|
||||
assert_equal(2, assign_score_of([123]))
|
||||
assert_equal(6, assign_score_of([123, 'abcd']))
|
||||
end
|
||||
|
||||
def test_assign_score_of_hash
|
||||
assert_equal(1, assign_score_of({}))
|
||||
assert_equal(5, assign_score_of('int' => 123))
|
||||
assert_equal(12, assign_score_of('int' => 123, 'str' => 'abcd'))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
class ObjectWrapperDrop < Liquid::Drop
|
||||
def initialize(obj)
|
||||
@obj = obj
|
||||
end
|
||||
with_error_mode(:lax) do
|
||||
assert Template.parse("{% assign foo = ('X' | downcase) %}")
|
||||
|
||||
def value
|
||||
@obj
|
||||
end
|
||||
end
|
||||
end # AssignTest
|
||||
|
||||
def assign_score_of(obj)
|
||||
context = Liquid::Context.new('drop' => ObjectWrapperDrop.new(obj))
|
||||
Liquid::Template.parse('{% assign obj = drop.value %}').render!(context)
|
||||
context.resource_limits.assign_score
|
||||
end
|
||||
end
|
||||
|
@ -1,16 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class FoobarTag < Liquid::Tag
|
||||
def render(*args)
|
||||
" "
|
||||
end
|
||||
|
||||
Liquid::Template.register_tag('foobar', FoobarTag)
|
||||
end
|
||||
|
||||
class BlankTestFileSystem
|
||||
def read_template_file(template_path, context)
|
||||
template_path
|
||||
def render_to_output_buffer(_context, output)
|
||||
output << ' '
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
@ -31,7 +26,9 @@ class BlankTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_new_tags_are_not_blank_by_default
|
||||
assert_template_result(" "*N, wrap_in_for("{% foobar %}"))
|
||||
with_custom_tag('foobar', FoobarTag) do
|
||||
assert_equal(" " * N, Liquid::Template.parse(wrap_in_for("{% foobar %}")).render!)
|
||||
end
|
||||
end
|
||||
|
||||
def test_loops_are_blank
|
||||
@ -47,7 +44,7 @@ class BlankTest < Minitest::Test
|
||||
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 +57,11 @@ class BlankTest < Minitest::Test
|
||||
|
||||
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 +75,42 @@ class BlankTest < Minitest::Test
|
||||
|
||||
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' %}"),
|
||||
partials: { 'foobar' => 'foobar' },
|
||||
)
|
||||
assert_template_result(
|
||||
" foobar " * (N + 1),
|
||||
wrap("{% include ' foobar ' %}"),
|
||||
partials: { ' foobar ' => ' foobar ' },
|
||||
)
|
||||
assert_template_result(
|
||||
" " * (N + 1),
|
||||
wrap(" {% include ' ' %} "),
|
||||
partials: { ' ' => ' ' },
|
||||
)
|
||||
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
|
||||
|
56
test/integration/block_test.rb
Normal file
56
test/integration/block_test.rb
Normal file
@ -0,0 +1,56 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class BlockTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_unexpected_end_tag
|
||||
source = '{% if true %}{% endunless %}'
|
||||
assert_match_syntax_error("Liquid syntax error (line 1): 'endunless' is not a valid delimiter for if tags. use endif", source)
|
||||
end
|
||||
|
||||
def test_with_custom_tag
|
||||
with_custom_tag('testtag', Block) do
|
||||
assert(Liquid::Template.parse("{% testtag %} {% endtesttag %}"))
|
||||
end
|
||||
end
|
||||
|
||||
def test_custom_block_tags_have_a_default_render_to_output_buffer_method_for_backwards_compatibility
|
||||
klass1 = Class.new(Block) do
|
||||
def render(*)
|
||||
'hello'
|
||||
end
|
||||
end
|
||||
|
||||
with_custom_tag('blabla', klass1) do
|
||||
template = Liquid::Template.parse("{% blabla %} bla {% endblabla %}")
|
||||
|
||||
assert_equal('hello', template.render)
|
||||
|
||||
buf = +''
|
||||
output = template.render({}, output: buf)
|
||||
assert_equal('hello', output)
|
||||
assert_equal('hello', buf)
|
||||
assert_equal(buf.object_id, output.object_id)
|
||||
end
|
||||
|
||||
klass2 = Class.new(klass1) do
|
||||
def render(*)
|
||||
'foo' + super + 'bar'
|
||||
end
|
||||
end
|
||||
|
||||
with_custom_tag('blabla', klass2) do
|
||||
template = Liquid::Template.parse("{% blabla %} foo {% endblabla %}")
|
||||
|
||||
assert_equal('foohellobar', template.render)
|
||||
|
||||
buf = +''
|
||||
output = template.render({}, output: buf)
|
||||
assert_equal('foohellobar', output)
|
||||
assert_equal('foohellobar', buf)
|
||||
assert_equal(buf.object_id, output.object_id)
|
||||
end
|
||||
end
|
||||
end
|
@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class CaptureTest < Minitest::Test
|
||||
@ -7,34 +9,44 @@ class CaptureTest < Minitest::Test
|
||||
assert_template_result("test string", "{% capture 'var' %}test string{% endcapture %}{{var}}", {})
|
||||
end
|
||||
|
||||
def test_capture_to_variable_from_outer_scope_if_existing
|
||||
template_source = <<-END_TEMPLATE
|
||||
{% assign var = '' %}
|
||||
{% if true %}
|
||||
{% capture var %}first-block-string{% endcapture %}
|
||||
{% endif %}
|
||||
{% if true %}
|
||||
{% capture var %}test-string{% endcapture %}
|
||||
{% endif %}
|
||||
{{var}}
|
||||
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 "test-string", rendered.gsub(/\s/, '')
|
||||
assert_template_result("Print this-thing", template_source)
|
||||
end
|
||||
|
||||
def test_capture_to_variable_from_outer_scope_if_existing
|
||||
template_source = <<~END_TEMPLATE
|
||||
{% assign var = '' -%}
|
||||
{% if true -%}
|
||||
{% capture var %}first-block-string{% endcapture -%}
|
||||
{% endif -%}
|
||||
{% if true -%}
|
||||
{% capture var %}test-string{% endcapture -%}
|
||||
{% endif -%}
|
||||
{{var-}}
|
||||
END_TEMPLATE
|
||||
assert_template_result("test-string", template_source)
|
||||
end
|
||||
|
||||
def test_assigning_from_capture
|
||||
template_source = <<-END_TEMPLATE
|
||||
{% assign first = '' %}
|
||||
{% assign second = '' %}
|
||||
{% for number in (1..3) %}
|
||||
{% capture first %}{{number}}{% endcapture %}
|
||||
{% assign second = first %}
|
||||
{% endfor %}
|
||||
{{ first }}-{{ second }}
|
||||
template_source = <<~END_TEMPLATE
|
||||
{% assign first = '' -%}
|
||||
{% assign second = '' -%}
|
||||
{% for number in (1..3) -%}
|
||||
{% capture first %}{{number}}{% endcapture -%}
|
||||
{% assign second = first -%}
|
||||
{% endfor -%}
|
||||
{{ first }}-{{ second -}}
|
||||
END_TEMPLATE
|
||||
template = Template.parse(template_source)
|
||||
rendered = template.render!
|
||||
assert_equal "3-3", rendered.gsub(/\s/, '')
|
||||
assert_template_result("3-3", template_source)
|
||||
end
|
||||
end # CaptureTest
|
||||
|
||||
def test_increment_assign_score_by_bytes_not_characters
|
||||
t = Template.parse("{% capture foo %}すごい{% endcapture %}")
|
||||
t.render!
|
||||
assert_equal(9, t.resource_limits.assign_score)
|
||||
end
|
||||
end
|
||||
|
@ -1,8 +1,599 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class HundredCentes
|
||||
def to_liquid
|
||||
100
|
||||
end
|
||||
end
|
||||
|
||||
class CentsDrop < Liquid::Drop
|
||||
def amount
|
||||
HundredCentes.new
|
||||
end
|
||||
|
||||
def non_zero?
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
class ContextSensitiveDrop < Liquid::Drop
|
||||
def test
|
||||
@context['test']
|
||||
end
|
||||
end
|
||||
|
||||
class Category
|
||||
attr_accessor :name
|
||||
|
||||
def initialize(name)
|
||||
@name = name
|
||||
end
|
||||
|
||||
def to_liquid
|
||||
CategoryDrop.new(self)
|
||||
end
|
||||
end
|
||||
|
||||
class CategoryDrop < Liquid::Drop
|
||||
attr_accessor :category, :context
|
||||
|
||||
def initialize(category)
|
||||
@category = category
|
||||
end
|
||||
end
|
||||
|
||||
class CounterDrop < Liquid::Drop
|
||||
def count
|
||||
@count ||= 0
|
||||
@count += 1
|
||||
end
|
||||
end
|
||||
|
||||
class ArrayLike
|
||||
def fetch(index)
|
||||
end
|
||||
|
||||
def [](index)
|
||||
@counts ||= []
|
||||
@counts[index] ||= 0
|
||||
@counts[index] += 1
|
||||
end
|
||||
|
||||
def to_liquid
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
class ContextTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def setup
|
||||
@context = Liquid::Context.new
|
||||
end
|
||||
|
||||
def test_variables
|
||||
@context['string'] = 'string'
|
||||
assert_equal('string', @context['string'])
|
||||
|
||||
@context['num'] = 5
|
||||
assert_equal(5, @context['num'])
|
||||
|
||||
@context['time'] = Time.parse('2006-06-06 12:00:00')
|
||||
assert_equal(Time.parse('2006-06-06 12:00:00'), @context['time'])
|
||||
|
||||
@context['date'] = Date.today
|
||||
assert_equal(Date.today, @context['date'])
|
||||
|
||||
now = Time.now
|
||||
@context['datetime'] = now
|
||||
assert_equal(now, @context['datetime'])
|
||||
|
||||
@context['bool'] = true
|
||||
assert_equal(true, @context['bool'])
|
||||
|
||||
@context['bool'] = false
|
||||
assert_equal(false, @context['bool'])
|
||||
|
||||
@context['nil'] = nil
|
||||
assert_nil(@context['nil'])
|
||||
assert_nil(@context['nil'])
|
||||
end
|
||||
|
||||
def test_variables_not_existing
|
||||
assert_template_result("true", "{% if does_not_exist == nil %}true{% endif %}")
|
||||
end
|
||||
|
||||
def test_scoping
|
||||
@context.push
|
||||
@context.pop
|
||||
|
||||
assert_raises(Liquid::ContextError) do
|
||||
@context.pop
|
||||
end
|
||||
|
||||
assert_raises(Liquid::ContextError) do
|
||||
@context.push
|
||||
@context.pop
|
||||
@context.pop
|
||||
end
|
||||
end
|
||||
|
||||
def test_length_query
|
||||
assert_template_result(
|
||||
"true",
|
||||
"{% if numbers.size == 4 %}true{% endif %}",
|
||||
{ "numbers" => [1, 2, 3, 4] },
|
||||
)
|
||||
|
||||
assert_template_result(
|
||||
"true",
|
||||
"{% if numbers.size == 4 %}true{% endif %}",
|
||||
{ "numbers" => { 1 => 1, 2 => 2, 3 => 3, 4 => 4 } },
|
||||
)
|
||||
|
||||
assert_template_result(
|
||||
"true",
|
||||
"{% if numbers.size == 1000 %}true{% endif %}",
|
||||
{ "numbers" => { 1 => 1, 2 => 2, 3 => 3, 4 => 4, 'size' => 1000 } },
|
||||
)
|
||||
end
|
||||
|
||||
def test_hyphenated_variable
|
||||
assert_template_result("godz", "{{ oh-my }}", { "oh-my" => 'godz' })
|
||||
end
|
||||
|
||||
def test_add_filter
|
||||
filter = Module.new do
|
||||
def hi(output)
|
||||
output + ' hi!'
|
||||
end
|
||||
end
|
||||
|
||||
context = Context.new
|
||||
context.add_filters(filter)
|
||||
assert_equal('hi? hi!', context.invoke(:hi, 'hi?'))
|
||||
|
||||
context = Context.new
|
||||
assert_equal('hi?', context.invoke(:hi, 'hi?'))
|
||||
|
||||
context.add_filters(filter)
|
||||
assert_equal('hi? hi!', context.invoke(:hi, 'hi?'))
|
||||
end
|
||||
|
||||
def test_only_intended_filters_make_it_there
|
||||
filter = Module.new do
|
||||
def hi(output)
|
||||
output + ' hi!'
|
||||
end
|
||||
end
|
||||
|
||||
context = Context.new
|
||||
assert_equal("Wookie", context.invoke("hi", "Wookie"))
|
||||
|
||||
context.add_filters(filter)
|
||||
assert_equal("Wookie hi!", context.invoke("hi", "Wookie"))
|
||||
end
|
||||
|
||||
def test_add_item_in_outer_scope
|
||||
@context['test'] = 'test'
|
||||
@context.push
|
||||
assert_equal('test', @context['test'])
|
||||
@context.pop
|
||||
assert_equal('test', @context['test'])
|
||||
end
|
||||
|
||||
def test_add_item_in_inner_scope
|
||||
@context.push
|
||||
@context['test'] = 'test'
|
||||
assert_equal('test', @context['test'])
|
||||
@context.pop
|
||||
assert_nil(@context['test'])
|
||||
end
|
||||
|
||||
def test_hierachical_data
|
||||
assigns = { 'hash' => { "name" => 'tobi' } }
|
||||
assert_template_result("tobi", "{{ hash.name }}", assigns)
|
||||
assert_template_result("tobi", '{{ hash["name"] }}', assigns)
|
||||
end
|
||||
|
||||
def test_keywords
|
||||
assert_template_result("pass", "{% if true == expect %}pass{% endif %}", { "expect" => true })
|
||||
assert_template_result("pass", "{% if false == expect %}pass{% endif %}", { "expect" => false })
|
||||
end
|
||||
|
||||
def test_digits
|
||||
assert_template_result("pass", "{% if 100 == expect %}pass{% endif %}", { "expect" => 100 })
|
||||
assert_template_result("pass", "{% if 100.00 == expect %}pass{% endif %}", { "expect" => 100.00 })
|
||||
end
|
||||
|
||||
def test_strings
|
||||
assert_template_result("hello!", '{{ "hello!" }}')
|
||||
assert_template_result("hello!", "{{ 'hello!' }}")
|
||||
end
|
||||
|
||||
def test_merge
|
||||
@context.merge("test" => "test")
|
||||
assert_equal('test', @context['test'])
|
||||
@context.merge("test" => "newvalue", "foo" => "bar")
|
||||
assert_equal('newvalue', @context['test'])
|
||||
assert_equal('bar', @context['foo'])
|
||||
end
|
||||
|
||||
def test_array_notation
|
||||
assigns = { "test" => ["a", "b"] }
|
||||
assert_template_result("a", "{{ test[0] }}", assigns)
|
||||
assert_template_result("b", "{{ test[1] }}", assigns)
|
||||
assert_template_result("pass", "{% if test[2] == nil %}pass{% endif %}", assigns)
|
||||
end
|
||||
|
||||
def test_recoursive_array_notation
|
||||
assigns = { "test" => { 'test' => [1, 2, 3, 4, 5] } }
|
||||
assert_template_result("1", "{{ test.test[0] }}", assigns)
|
||||
|
||||
assigns = { "test" => [{ 'test' => 'worked' }] }
|
||||
assert_template_result("worked", "{{ test[0].test }}", assigns)
|
||||
end
|
||||
|
||||
def test_hash_to_array_transition
|
||||
assigns = {
|
||||
'colors' => {
|
||||
'Blue' => ['003366', '336699', '6699CC', '99CCFF'],
|
||||
'Green' => ['003300', '336633', '669966', '99CC99'],
|
||||
'Yellow' => ['CC9900', 'FFCC00', 'FFFF99', 'FFFFCC'],
|
||||
'Red' => ['660000', '993333', 'CC6666', 'FF9999'],
|
||||
},
|
||||
}
|
||||
|
||||
assert_template_result("003366", "{{ colors.Blue[0] }}", assigns)
|
||||
assert_template_result("FF9999", "{{ colors.Red[3] }}", assigns)
|
||||
end
|
||||
|
||||
def test_try_first
|
||||
assigns = { 'test' => [1, 2, 3, 4, 5] }
|
||||
assert_template_result("1", "{{ test.first }}", assigns)
|
||||
assert_template_result("pass", "{% if test.last == 5 %}pass{% endif %}", assigns)
|
||||
|
||||
assigns = { "test" => { "test" => [1, 2, 3, 4, 5] } }
|
||||
assert_template_result("1", "{{ test.test.first }}", assigns)
|
||||
assert_template_result("5", "{{ test.test.last }}", assigns)
|
||||
|
||||
assigns = { "test" => [1] }
|
||||
assert_template_result("1", "{{ test.first }}", assigns)
|
||||
assert_template_result("1", "{{ test.last }}", assigns)
|
||||
end
|
||||
|
||||
def test_access_hashes_with_hash_notation
|
||||
assigns = { 'products' => { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] } }
|
||||
assert_template_result("5", '{{ products["count"] }}', assigns)
|
||||
assert_template_result("deepsnow", '{{ products["tags"][0] }}', assigns)
|
||||
assert_template_result("deepsnow", '{{ products["tags"].first }}', assigns)
|
||||
|
||||
assigns = { 'product' => { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] } }
|
||||
assert_template_result("draft151cm", '{{ product["variants"][0]["title"] }}', assigns)
|
||||
assert_template_result("element151cm", '{{ product["variants"][1]["title"] }}', assigns)
|
||||
assert_template_result("draft151cm", '{{ product["variants"].first["title"] }}', assigns)
|
||||
assert_template_result("element151cm", '{{ product["variants"].last["title"] }}', assigns)
|
||||
end
|
||||
|
||||
def test_access_variable_with_hash_notation
|
||||
assert_template_result('baz', '{{ ["foo"] }}', { "foo" => "baz" })
|
||||
assert_template_result('baz', '{{ [bar] }}', { 'foo' => 'baz', 'bar' => 'foo' })
|
||||
end
|
||||
|
||||
def test_access_hashes_with_hash_access_variables
|
||||
assigns = {
|
||||
'var' => 'tags',
|
||||
'nested' => { 'var' => 'tags' },
|
||||
'products' => { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] },
|
||||
}
|
||||
|
||||
assert_template_result('deepsnow', '{{ products[var].first }}', assigns)
|
||||
assert_template_result('freestyle', '{{ products[nested.var].last }}', assigns)
|
||||
end
|
||||
|
||||
def test_hash_notation_only_for_hash_access
|
||||
assigns = { "array" => [1, 2, 3, 4, 5] }
|
||||
assert_template_result("1", "{{ array.first }}", assigns)
|
||||
assert_template_result("pass", '{% if array["first"] == nil %}pass{% endif %}', assigns)
|
||||
|
||||
assert_template_result("Hello", '{{ hash["first"] }}', { "hash" => { "first" => "Hello" } })
|
||||
end
|
||||
|
||||
def test_first_can_appear_in_middle_of_callchain
|
||||
assigns = { "product" => { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] } }
|
||||
|
||||
assert_template_result('draft151cm', '{{ product.variants[0].title }}', assigns)
|
||||
assert_template_result('element151cm', '{{ product.variants[1].title }}', assigns)
|
||||
assert_template_result('draft151cm', '{{ product.variants.first.title }}', assigns)
|
||||
assert_template_result('element151cm', '{{ product.variants.last.title }}', assigns)
|
||||
end
|
||||
|
||||
def test_cents
|
||||
@context.merge("cents" => HundredCentes.new)
|
||||
assert_equal(100, @context['cents'])
|
||||
end
|
||||
|
||||
def test_nested_cents
|
||||
@context.merge("cents" => { 'amount' => HundredCentes.new })
|
||||
assert_equal(100, @context['cents.amount'])
|
||||
|
||||
@context.merge("cents" => { 'cents' => { 'amount' => HundredCentes.new } })
|
||||
assert_equal(100, @context['cents.cents.amount'])
|
||||
end
|
||||
|
||||
def test_cents_through_drop
|
||||
@context.merge("cents" => CentsDrop.new)
|
||||
assert_equal(100, @context['cents.amount'])
|
||||
end
|
||||
|
||||
def test_nested_cents_through_drop
|
||||
@context.merge("vars" => { "cents" => CentsDrop.new })
|
||||
assert_equal(100, @context['vars.cents.amount'])
|
||||
end
|
||||
|
||||
def test_drop_methods_with_question_marks
|
||||
@context.merge("cents" => CentsDrop.new)
|
||||
assert(@context['cents.non_zero?'])
|
||||
end
|
||||
|
||||
def test_context_from_within_drop
|
||||
@context.merge("test" => '123', "vars" => ContextSensitiveDrop.new)
|
||||
assert_equal('123', @context['vars.test'])
|
||||
end
|
||||
|
||||
def test_nested_context_from_within_drop
|
||||
@context.merge("test" => '123', "vars" => { "local" => ContextSensitiveDrop.new })
|
||||
assert_equal('123', @context['vars.local.test'])
|
||||
end
|
||||
|
||||
def test_ranges
|
||||
assert_template_result("1..5", '{{ (1..5) }}')
|
||||
assert_template_result("pass", '{% if (1..5) == expect %}pass{% endif %}', { "expect" => (1..5) })
|
||||
|
||||
assigns = { "test" => '5' }
|
||||
assert_template_result("1..5", "{{ (1..test) }}", assigns)
|
||||
assert_template_result("5..5", "{{ (test..test) }}", assigns)
|
||||
end
|
||||
|
||||
def test_cents_through_drop_nestedly
|
||||
@context.merge("cents" => { "cents" => CentsDrop.new })
|
||||
assert_equal(100, @context['cents.cents.amount'])
|
||||
|
||||
@context.merge("cents" => { "cents" => { "cents" => CentsDrop.new } })
|
||||
assert_equal(100, @context['cents.cents.cents.amount'])
|
||||
end
|
||||
|
||||
def test_drop_with_variable_called_only_once
|
||||
@context['counter'] = CounterDrop.new
|
||||
|
||||
assert_equal(1, @context['counter.count'])
|
||||
assert_equal(2, @context['counter.count'])
|
||||
assert_equal(3, @context['counter.count'])
|
||||
end
|
||||
|
||||
def test_drop_with_key_called_only_once
|
||||
@context['counter'] = CounterDrop.new
|
||||
|
||||
assert_equal(1, @context['counter["count"]'])
|
||||
assert_equal(2, @context['counter["count"]'])
|
||||
assert_equal(3, @context['counter["count"]'])
|
||||
end
|
||||
|
||||
def test_proc_as_variable
|
||||
@context['dynamic'] = proc { 'Hello' }
|
||||
|
||||
assert_equal('Hello', @context['dynamic'])
|
||||
end
|
||||
|
||||
def test_lambda_as_variable
|
||||
@context['dynamic'] = proc { 'Hello' }
|
||||
|
||||
assert_equal('Hello', @context['dynamic'])
|
||||
end
|
||||
|
||||
def test_nested_lambda_as_variable
|
||||
@context['dynamic'] = { "lambda" => proc { 'Hello' } }
|
||||
|
||||
assert_equal('Hello', @context['dynamic.lambda'])
|
||||
end
|
||||
|
||||
def test_array_containing_lambda_as_variable
|
||||
@context['dynamic'] = [1, 2, proc { 'Hello' }, 4, 5]
|
||||
|
||||
assert_equal('Hello', @context['dynamic[2]'])
|
||||
end
|
||||
|
||||
def test_lambda_is_called_once
|
||||
@global = 0
|
||||
|
||||
@context['callcount'] = proc {
|
||||
@global += 1
|
||||
@global.to_s
|
||||
}
|
||||
|
||||
assert_equal('1', @context['callcount'])
|
||||
assert_equal('1', @context['callcount'])
|
||||
assert_equal('1', @context['callcount'])
|
||||
end
|
||||
|
||||
def test_nested_lambda_is_called_once
|
||||
@global = 0
|
||||
|
||||
@context['callcount'] = {
|
||||
"lambda" => proc {
|
||||
@global += 1
|
||||
@global.to_s
|
||||
},
|
||||
}
|
||||
|
||||
assert_equal('1', @context['callcount.lambda'])
|
||||
assert_equal('1', @context['callcount.lambda'])
|
||||
assert_equal('1', @context['callcount.lambda'])
|
||||
end
|
||||
|
||||
def test_lambda_in_array_is_called_once
|
||||
@global = 0
|
||||
|
||||
p = proc {
|
||||
@global += 1
|
||||
@global.to_s
|
||||
}
|
||||
@context['callcount'] = [1, 2, p, 4, 5]
|
||||
|
||||
assert_equal('1', @context['callcount[2]'])
|
||||
assert_equal('1', @context['callcount[2]'])
|
||||
assert_equal('1', @context['callcount[2]'])
|
||||
end
|
||||
|
||||
def test_access_to_context_from_proc
|
||||
@context.registers[:magic] = 345392
|
||||
|
||||
@context['magic'] = proc { @context.registers[:magic] }
|
||||
|
||||
assert_equal(345392, @context['magic'])
|
||||
end
|
||||
|
||||
def test_to_liquid_and_context_at_first_level
|
||||
@context['category'] = Category.new("foobar")
|
||||
assert_kind_of(CategoryDrop, @context['category'])
|
||||
assert_equal(@context, @context['category'].context)
|
||||
end
|
||||
|
||||
def test_interrupt_avoids_object_allocations
|
||||
@context.interrupt? # ruby 3.0.0 allocates on the first call
|
||||
assert_no_object_allocations do
|
||||
@context.interrupt?
|
||||
end
|
||||
end
|
||||
|
||||
def test_context_initialization_with_a_proc_in_environment
|
||||
contx = Context.new([test: ->(c) { c['poutine'] }], test: :foo)
|
||||
|
||||
assert(contx)
|
||||
assert_nil(contx['poutine'])
|
||||
end
|
||||
|
||||
def test_apply_global_filter
|
||||
global_filter_proc = ->(output) { "#{output} filtered" }
|
||||
|
||||
context = Context.new
|
||||
context.global_filter = global_filter_proc
|
||||
|
||||
assert_equal('hi filtered', context.apply_global_filter('hi'))
|
||||
end
|
||||
|
||||
def test_static_environments_are_read_with_lower_priority_than_environments
|
||||
context = Context.build(
|
||||
static_environments: { 'shadowed' => 'static', 'unshadowed' => 'static' },
|
||||
environments: { 'shadowed' => 'dynamic' },
|
||||
)
|
||||
|
||||
assert_equal('dynamic', context['shadowed'])
|
||||
assert_equal('static', context['unshadowed'])
|
||||
end
|
||||
|
||||
def test_apply_global_filter_when_no_global_filter_exist
|
||||
context = Context.new
|
||||
assert_equal('hi', context.apply_global_filter('hi'))
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_does_not_inherit_variables
|
||||
super_context = Context.new
|
||||
super_context['my_variable'] = 'some value'
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
|
||||
assert_nil(subcontext['my_variable'])
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_inherits_static_environment
|
||||
super_context = Context.build(static_environments: { 'my_environment_value' => 'my value' })
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
|
||||
assert_equal('my value', subcontext['my_environment_value'])
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_inherits_resource_limits
|
||||
resource_limits = ResourceLimits.new({})
|
||||
super_context = Context.new({}, {}, {}, false, resource_limits)
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
assert_equal(resource_limits, subcontext.resource_limits)
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_inherits_exception_renderer
|
||||
super_context = Context.new
|
||||
super_context.exception_renderer = ->(_e) { 'my exception message' }
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
assert_equal('my exception message', subcontext.handle_error(Liquid::Error.new))
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_does_not_inherit_non_static_registers
|
||||
registers = {
|
||||
my_register: :my_value,
|
||||
}
|
||||
super_context = Context.new({}, {}, Registers.new(registers))
|
||||
super_context.registers[:my_register] = :my_alt_value
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
assert_equal(:my_value, subcontext.registers[:my_register])
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_inherits_static_registers
|
||||
super_context = Context.build(registers: { my_register: :my_value })
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
assert_equal(:my_value, subcontext.registers[:my_register])
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_registers_do_not_pollute_context
|
||||
super_context = Context.build(registers: { my_register: :my_value })
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
subcontext.registers[:my_register] = :my_alt_value
|
||||
assert_equal(:my_value, super_context.registers[:my_register])
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_inherits_filters
|
||||
my_filter = Module.new do
|
||||
def my_filter(*)
|
||||
'my filter result'
|
||||
end
|
||||
end
|
||||
|
||||
super_context = Context.new
|
||||
super_context.add_filters([my_filter])
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
template = Template.parse('{{ 123 | my_filter }}')
|
||||
assert_equal('my filter result', template.render(subcontext))
|
||||
end
|
||||
|
||||
def test_disables_tag_specified
|
||||
context = Context.new
|
||||
context.with_disabled_tags(%w(foo bar)) do
|
||||
assert_equal(true, context.tag_disabled?("foo"))
|
||||
assert_equal(true, context.tag_disabled?("bar"))
|
||||
assert_equal(false, context.tag_disabled?("unknown"))
|
||||
end
|
||||
end
|
||||
|
||||
def test_disables_nested_tags
|
||||
context = Context.new
|
||||
context.with_disabled_tags(["foo"]) do
|
||||
context.with_disabled_tags(["foo"]) do
|
||||
assert_equal(true, context.tag_disabled?("foo"))
|
||||
assert_equal(false, context.tag_disabled?("bar"))
|
||||
end
|
||||
context.with_disabled_tags(["bar"]) do
|
||||
assert_equal(true, context.tag_disabled?("foo"))
|
||||
assert_equal(true, context.tag_disabled?("bar"))
|
||||
context.with_disabled_tags(["foo"]) do
|
||||
assert_equal(true, context.tag_disabled?("foo"))
|
||||
assert_equal(true, context.tag_disabled?("bar"))
|
||||
end
|
||||
end
|
||||
assert_equal(true, context.tag_disabled?("foo"))
|
||||
assert_equal(false, context.tag_disabled?("bar"))
|
||||
end
|
||||
end
|
||||
|
||||
def test_override_global_filter
|
||||
global = Module.new do
|
||||
def notice(output)
|
||||
@ -17,16 +608,44 @@ class ContextTest < Minitest::Test
|
||||
end
|
||||
|
||||
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])
|
||||
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
|
||||
with_error_mode :strict do
|
||||
with_error_mode(:strict) do
|
||||
context = Context.new
|
||||
context.has_key?('unknown')
|
||||
assert_empty context.errors
|
||||
context.key?('unknown')
|
||||
assert_empty(context.errors)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_context_always_uses_static_registers
|
||||
registers = {
|
||||
my_register: :my_value,
|
||||
}
|
||||
c = Context.new({}, {}, registers)
|
||||
assert_instance_of(Registers, c.registers)
|
||||
assert_equal(:my_value, c.registers[:my_register])
|
||||
|
||||
r = Registers.new(registers)
|
||||
c = Context.new({}, {}, r)
|
||||
assert_instance_of(Registers, c.registers)
|
||||
assert_equal(:my_value, c.registers[:my_register])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def assert_no_object_allocations
|
||||
unless RUBY_ENGINE == 'ruby'
|
||||
skip("stackprof needed to count object allocations")
|
||||
end
|
||||
require 'stackprof'
|
||||
|
||||
profile = StackProf.run(mode: :object) do
|
||||
yield
|
||||
end
|
||||
assert_equal(0, profile[:samples])
|
||||
end
|
||||
end # ContextTest
|
||||
|
@ -1,19 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
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"
|
||||
source = "{% else %}"
|
||||
assert_match_syntax_error("Liquid syntax error (line 1): Unexpected outer 'else' tag", source)
|
||||
end
|
||||
|
||||
def test_unknown_tag
|
||||
exc = assert_raises(SyntaxError) do
|
||||
Template.parse("{% foo %}")
|
||||
end
|
||||
assert_equal exc.message, "Liquid syntax error: Unknown tag 'foo'"
|
||||
source = "{% foo %}"
|
||||
assert_match_syntax_error("Liquid syntax error (line 1): Unknown tag 'foo'", source)
|
||||
end
|
||||
end
|
||||
|
@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class ContextDrop < Liquid::Drop
|
||||
@ -13,13 +15,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 +32,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}"
|
||||
end
|
||||
end
|
||||
|
||||
@ -48,18 +49,15 @@ 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
|
||||
|
||||
@ -93,7 +91,7 @@ end
|
||||
class RealEnumerableDrop < Liquid::Drop
|
||||
include Enumerable
|
||||
|
||||
def before_method(method)
|
||||
def liquid_method_missing(method)
|
||||
method
|
||||
end
|
||||
|
||||
@ -109,163 +107,151 @@ class DropsTest < Minitest::Test
|
||||
|
||||
def test_product_drop
|
||||
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 }}')
|
||||
tpl.render!('product' => ProductDrop.new)
|
||||
assert_match /tainted/, tpl.warnings.first
|
||||
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
|
||||
assert_equal(' ', tpl.render!('product' => ProductDrop.new))
|
||||
end
|
||||
|
||||
def test_drop_does_only_respond_to_whitelisted_methods
|
||||
assert_equal "", Liquid::Template.parse("{{ product.inspect }}").render!('product' => ProductDrop.new)
|
||||
assert_equal "", Liquid::Template.parse("{{ product.pretty_inspect }}").render!('product' => ProductDrop.new)
|
||||
assert_equal "", Liquid::Template.parse("{{ product.whatever }}").render!('product' => ProductDrop.new)
|
||||
assert_equal "", Liquid::Template.parse('{{ product | map: "inspect" }}').render!('product' => ProductDrop.new)
|
||||
assert_equal "", Liquid::Template.parse('{{ product | map: "pretty_inspect" }}').render!('product' => ProductDrop.new)
|
||||
assert_equal "", Liquid::Template.parse('{{ product | map: "whatever" }}').render!('product' => ProductDrop.new)
|
||||
assert_equal("", Liquid::Template.parse("{{ product.inspect }}").render!('product' => ProductDrop.new))
|
||||
assert_equal("", Liquid::Template.parse("{{ product.pretty_inspect }}").render!('product' => ProductDrop.new))
|
||||
assert_equal("", Liquid::Template.parse("{{ product.whatever }}").render!('product' => ProductDrop.new))
|
||||
assert_equal("", Liquid::Template.parse('{{ product | map: "inspect" }}').render!('product' => ProductDrop.new))
|
||||
assert_equal("", Liquid::Template.parse('{{ product | map: "pretty_inspect" }}').render!('product' => ProductDrop.new))
|
||||
assert_equal("", Liquid::Template.parse('{{ product | map: "whatever" }}').render!('product' => ProductDrop.new))
|
||||
end
|
||||
|
||||
def test_drops_respond_to_to_liquid
|
||||
assert_equal "text1", Liquid::Template.parse("{{ product.to_liquid.texts.text }}").render!('product' => ProductDrop.new)
|
||||
assert_equal "text1", Liquid::Template.parse('{{ product | map: "to_liquid" | map: "texts" | map: "text" }}').render!('product' => ProductDrop.new)
|
||||
assert_equal("text1", Liquid::Template.parse("{{ product.to_liquid.texts.text }}").render!('product' => ProductDrop.new))
|
||||
assert_equal("text1", Liquid::Template.parse('{{ product | map: "to_liquid" | map: "texts" | map: "text" }}').render!('product' => ProductDrop.new))
|
||||
end
|
||||
|
||||
def test_text_drop
|
||||
output = Liquid::Template.parse( ' {{ product.texts.text }} ' ).render!('product' => ProductDrop.new)
|
||||
assert_equal ' text1 ', output
|
||||
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)
|
||||
assert_equal ' text1 text2 ', output
|
||||
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")
|
||||
assert_equal ' carrot ', output
|
||||
output = Liquid::Template.parse(' {{ context.bar }} ').render!('context' => ContextDrop.new, 'bar' => "carrot")
|
||||
assert_equal(' carrot ', output)
|
||||
end
|
||||
|
||||
def test_context_drop_array_with_map
|
||||
output = Liquid::Template.parse(' {{ contexts | map: "bar" }} ').render!('contexts' => [ContextDrop.new, ContextDrop.new], 'bar' => "carrot")
|
||||
assert_equal(' carrotcarrot ', output)
|
||||
end
|
||||
|
||||
def test_nested_context_drop
|
||||
output = Liquid::Template.parse( ' {{ product.context.foo }} ' ).render!('product' => ProductDrop.new, 'foo' => "monkey")
|
||||
assert_equal ' monkey ', output
|
||||
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)
|
||||
assert_equal ' ', output
|
||||
output = Liquid::Template.parse(' {{ product.callmenot }} ').render!('product' => ProductDrop.new)
|
||||
assert_equal(' ', output)
|
||||
end
|
||||
|
||||
def test_object_methods_not_allowed
|
||||
[:dup, :clone, :singleton_class, :eval, :class_eval, :inspect].each do |method|
|
||||
output = Liquid::Template.parse(" {{ product.#{method} }} ").render!('product' => ProductDrop.new)
|
||||
assert_equal ' ', output
|
||||
assert_equal(' ', output)
|
||||
end
|
||||
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)
|
||||
assert_equal method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new)
|
||||
assert_equal method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new)
|
||||
assert_equal(method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new))
|
||||
assert_equal(method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new))
|
||||
assert_equal(method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new))
|
||||
assert_equal(method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new))
|
||||
end
|
||||
end
|
||||
|
||||
def test_some_enumerable_methods_still_get_invoked
|
||||
[ :count, :max ].each do |method|
|
||||
assert_equal "3", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new)
|
||||
assert_equal "3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new)
|
||||
assert_equal "3", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)
|
||||
assert_equal "3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)
|
||||
[:count, :max].each do |method|
|
||||
assert_equal("3", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new))
|
||||
assert_equal("3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new))
|
||||
assert_equal("3", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new))
|
||||
assert_equal("3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new))
|
||||
end
|
||||
|
||||
assert_equal "yes", Liquid::Template.parse("{% if collection contains 3 %}yes{% endif %}").render!('collection' => RealEnumerableDrop.new)
|
||||
assert_equal("yes", Liquid::Template.parse("{% if collection contains 3 %}yes{% endif %}").render!('collection' => RealEnumerableDrop.new))
|
||||
|
||||
[ :min, :first ].each do |method|
|
||||
assert_equal "1", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new)
|
||||
assert_equal "1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new)
|
||||
assert_equal "1", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)
|
||||
assert_equal "1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)
|
||||
[:min, :first].each do |method|
|
||||
assert_equal("1", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new))
|
||||
assert_equal("1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new))
|
||||
assert_equal("1", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new))
|
||||
assert_equal("1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new))
|
||||
end
|
||||
end
|
||||
|
||||
def test_empty_string_value_access
|
||||
assert_equal '', Liquid::Template.parse('{{ product[value] }}').render!('product' => ProductDrop.new, 'value' => '')
|
||||
assert_equal('', Liquid::Template.parse('{{ product[value] }}').render!('product' => ProductDrop.new, 'value' => ''))
|
||||
end
|
||||
|
||||
def test_nil_value_access
|
||||
assert_equal '', Liquid::Template.parse('{{ product[value] }}').render!('product' => ProductDrop.new, 'value' => nil)
|
||||
assert_equal('', Liquid::Template.parse('{{ product[value] }}').render!('product' => ProductDrop.new, 'value' => nil))
|
||||
end
|
||||
|
||||
def test_default_to_s_on_drops
|
||||
assert_equal 'ProductDrop', Liquid::Template.parse("{{ product }}").render!('product' => ProductDrop.new)
|
||||
assert_equal 'EnumerableDrop', Liquid::Template.parse('{{ collection }}').render!('collection' => EnumerableDrop.new)
|
||||
assert_equal('ProductDrop', Liquid::Template.parse("{{ product }}").render!('product' => ProductDrop.new))
|
||||
assert_equal('EnumerableDrop', Liquid::Template.parse('{{ collection }}').render!('collection' => EnumerableDrop.new))
|
||||
end
|
||||
|
||||
def test_invokable_methods
|
||||
assert_equal(%w(to_liquid catchall context texts).to_set, ProductDrop.invokable_methods)
|
||||
assert_equal(%w(to_liquid scopes_as_array loop_pos scopes).to_set, ContextDrop.invokable_methods)
|
||||
assert_equal(%w(to_liquid size max min first count).to_set, EnumerableDrop.invokable_methods)
|
||||
assert_equal(%w(to_liquid max min sort count first).to_set, RealEnumerableDrop.invokable_methods)
|
||||
end
|
||||
end # DropsTest
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user