mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-15 04:10:40 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
626f911503 | ||
|
|
de32259bb9 | ||
|
|
d70cd2a1ea | ||
|
|
26b217542b | ||
|
|
e3ecfe3adc | ||
|
|
c16c787861 | ||
|
|
13f5f95d01 | ||
|
|
c8db5e6259 | ||
|
|
9e511ecd14 | ||
|
|
45d95bb8d8 | ||
|
|
8d3eca6ee2 | ||
|
|
6a0ad102a8 | ||
|
|
b8e4b3337b | ||
|
|
cd92e77726 | ||
|
|
e84fbf32ad | ||
|
|
0266b27838 | ||
|
|
090c88eb92 | ||
|
|
e15803fae4 | ||
|
|
117673a725 | ||
|
|
ef43120d6e | ||
|
|
f9f595f0f4 | ||
|
|
2bbf50171b | ||
|
|
a492d8e23d | ||
|
|
c8afa39a01 | ||
|
|
d8f9091a12 | ||
|
|
ca18a579a1 | ||
|
|
6f2b24f7ca | ||
|
|
87d7dd0d37 | ||
|
|
768fb79e32 | ||
|
|
c920ebb282 | ||
|
|
9706e8b411 | ||
|
|
75698eefec | ||
|
|
dfd358c0a2 | ||
|
|
4c70946fa1 | ||
|
|
9d9c0c1ca7 | ||
|
|
a9d3a5a402 | ||
|
|
30dffb675e | ||
|
|
e67c3c6924 | ||
|
|
e2e410f334 | ||
|
|
27ce44238e | ||
|
|
abfaf8a042 | ||
|
|
18a807ea2e | ||
|
|
a6126c3bea | ||
|
|
a96d772963 | ||
|
|
732ef2605e | ||
|
|
72ee7b4c1e | ||
|
|
f807383f2a | ||
|
|
02bfed1c57 | ||
|
|
b11df79b91 | ||
|
|
8ee9bd7ba8 | ||
|
|
5b3f4195f0 | ||
|
|
236d971705 | ||
|
|
0cd4d87d46 | ||
|
|
1fdfd4a68a | ||
|
|
c1f73a9f03 | ||
|
|
d8f550e447 | ||
|
|
2e5ab98bfc | ||
|
|
0dbb7792d7 | ||
|
|
e10ce657dd | ||
|
|
3713fa08b2 | ||
|
|
c5002d8cad | ||
|
|
00b0a5708c | ||
|
|
f82ee8a18c | ||
|
|
632bb6b077 | ||
|
|
0b6ee9bca1 | ||
|
|
941dd668fe | ||
|
|
11ffd65503 | ||
|
|
ce0280ca9f | ||
|
|
1066ef0f76 | ||
|
|
7b38c28d52 | ||
|
|
97e7983bd8 | ||
|
|
6cf6ffae90 |
@@ -287,6 +287,61 @@
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "TrySound",
|
||||
"name": "Bogdan Chadkin",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/5635476?v=4",
|
||||
"profile": "https://twitter.com/IAmTrySound",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "tejasmanohar",
|
||||
"name": "Tejas Manohar",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/5959235?v=4",
|
||||
"profile": "https://hightouch.io",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "pdehaan",
|
||||
"name": "Peter deHaan",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/557895?v=4",
|
||||
"profile": "http://about.me/peterdehaan",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "amit777",
|
||||
"name": "amit777",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/2703309?v=4",
|
||||
"profile": "https://github.com/amit777",
|
||||
"contributions": [
|
||||
"code",
|
||||
"financial"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "sschuldenzucker",
|
||||
"name": "Steffen Schuldenzucker",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/1100776?v=4",
|
||||
"profile": "http://www.ifi.uzh.ch/en/ce/people/schuldenzucker.html",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Pixcell",
|
||||
"name": "Pixcell",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/4005291?v=4",
|
||||
"profile": "https://github.com/Pixcell",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
@@ -1,3 +1,86 @@
|
||||
# [9.19.0](https://github.com/harttle/liquidjs/compare/v9.18.0...v9.19.0) (2020-12-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* lint ([de32259](https://github.com/harttle/liquidjs/commit/de32259))
|
||||
* move offset adding complexity inside TimezoneDate ([26b2175](https://github.com/harttle/liquidjs/commit/26b2175))
|
||||
* simpler timezone regex and non-null offset ([e3ecfe3](https://github.com/harttle/liquidjs/commit/e3ecfe3))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add preserveTimezones option ([d70cd2a](https://github.com/harttle/liquidjs/commit/d70cd2a))
|
||||
* parse and handle date timezone offsets ([c16c787](https://github.com/harttle/liquidjs/commit/c16c787))
|
||||
|
||||
# [9.18.0](https://github.com/harttle/liquidjs/compare/v9.17.0...v9.18.0) (2020-12-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* address refactor comments ([6a0ad10](https://github.com/harttle/liquidjs/commit/6a0ad10))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add option for keeping variable type in output ([cd92e77](https://github.com/harttle/liquidjs/commit/cd92e77))
|
||||
|
||||
# [9.17.0](https://github.com/harttle/liquidjs/compare/v9.16.1...v9.17.0) (2020-12-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* elsif is not supported for unless, fixes [#268](https://github.com/harttle/liquidjs/issues/268) ([2bbf501](https://github.com/harttle/liquidjs/commit/2bbf501))
|
||||
* enforce string-type pattern in `replace`, fixes [#243](https://github.com/harttle/liquidjs/issues/243) ([c8afa39](https://github.com/harttle/liquidjs/commit/c8afa39))
|
||||
* raw block not ignoring {% characters, fixes [#263](https://github.com/harttle/liquidjs/issues/263) ([a492d8e](https://github.com/harttle/liquidjs/commit/a492d8e))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* passing liquid to FilterImpl, closes [#277](https://github.com/harttle/liquidjs/issues/277) ([f9f595f](https://github.com/harttle/liquidjs/commit/f9f595f))
|
||||
|
||||
## [9.16.1](https://github.com/harttle/liquidjs/compare/v9.16.0...v9.16.1) (2020-10-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* braced property access ([18a807e](https://github.com/harttle/liquidjs/commit/18a807e))
|
||||
|
||||
# [9.16.0](https://github.com/harttle/liquidjs/compare/v9.15.1...v9.16.0) (2020-10-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* support jsTruthy, [#255](https://github.com/harttle/liquidjs/issues/255) [#257](https://github.com/harttle/liquidjs/issues/257) ([72ee7b4](https://github.com/harttle/liquidjs/commit/72ee7b4))
|
||||
|
||||
## [9.15.1](https://github.com/harttle/liquidjs/compare/v9.15.0...v9.15.1) (2020-10-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* allow quoted variable name in capture, fixes [#252](https://github.com/harttle/liquidjs/issues/252) ([5b3f419](https://github.com/harttle/liquidjs/commit/5b3f419))
|
||||
|
||||
# [9.15.0](https://github.com/harttle/liquidjs/compare/v9.14.1...v9.15.0) (2020-08-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* export toPromise and toValue, see [#158](https://github.com/harttle/liquidjs/issues/158) ([2e5ab98](https://github.com/harttle/liquidjs/commit/2e5ab98))
|
||||
|
||||
## [9.14.1](https://github.com/harttle/liquidjs/compare/v9.14.0...v9.14.1) (2020-07-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* enumerate Promises (e.g. in for & tablerow) ([#237](https://github.com/harttle/liquidjs/issues/237)) ([941dd66](https://github.com/harttle/liquidjs/commit/941dd66))
|
||||
|
||||
# [9.14.0](https://github.com/harttle/liquidjs/compare/v9.13.0...v9.14.0) (2020-06-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* setup universal browser and node builds ([6cf6ffa](https://github.com/harttle/liquidjs/commit/6cf6ffa))
|
||||
|
||||
# [9.13.0](https://github.com/harttle/liquidjs/compare/v9.12.0...v9.13.0) (2020-06-25)
|
||||
|
||||
|
||||
|
||||
@@ -59,44 +59,52 @@ Want to contribute? see [Contribution Guidelines][contribution]. Thanks goes to
|
||||
<!-- markdownlint-disable -->
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center"><a href="https://harttle.land"><img src="https://avatars3.githubusercontent.com/u/4427974?v=4" width="100px;" alt="Jun Yang"/><br /><sub><b>Jun Yang</b></sub></a><br /><a href="#maintenance-harttle" title="Maintenance">🚧</a> <a href="https://github.com/harttle/liquidjs/commits?author=harttle" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/chenos"><img src="https://avatars0.githubusercontent.com/u/2993310?v=4" width="100px;" alt="chenos"/><br /><sub><b>chenos</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=chenos" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://zachleat.com/"><img src="https://avatars2.githubusercontent.com/u/39355?v=4" width="100px;" alt="Zach Leatherman"/><br /><sub><b>Zach Leatherman</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/issues?q=author%3Azachleat" title="Bug reports">🐛</a></td>
|
||||
<td align="center"><a href="https://github.com/thardy"><img src="https://avatars3.githubusercontent.com/u/120636?v=4" width="100px;" alt="Tim Hardy"/><br /><sub><b>Tim Hardy</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=thardy" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://paulrobertlloyd.com/"><img src="https://avatars3.githubusercontent.com/u/813383?v=4" width="100px;" alt="Paul Robert Lloyd"/><br /><sub><b>Paul Robert Lloyd</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=paulrobertlloyd" title="Code">💻</a> <a href="https://github.com/harttle/liquidjs/issues?q=author%3Apaulrobertlloyd" title="Bug reports">🐛</a></td>
|
||||
<td align="center"><a href="https://twitter.com/alecdotbiz"><img src="https://avatars2.githubusercontent.com/u/1925840?v=4" width="100px;" alt="Alec Larson"/><br /><sub><b>Alec Larson</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=aleclarson" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/pmalouin"><img src="https://avatars1.githubusercontent.com/u/1411117?v=4" width="100px;" alt="Patrick Malouin"/><br /><sub><b>Patrick Malouin</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=pmalouin" title="Code">💻</a> <a href="https://github.com/harttle/liquidjs/commits?author=pmalouin" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://harttle.land"><img src="https://avatars3.githubusercontent.com/u/4427974?v=4" width="100px;" alt=""/><br /><sub><b>Jun Yang</b></sub></a><br /><a href="#maintenance-harttle" title="Maintenance">🚧</a> <a href="https://github.com/harttle/liquidjs/commits?author=harttle" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/chenos"><img src="https://avatars0.githubusercontent.com/u/2993310?v=4" width="100px;" alt=""/><br /><sub><b>chenos</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=chenos" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://zachleat.com/"><img src="https://avatars2.githubusercontent.com/u/39355?v=4" width="100px;" alt=""/><br /><sub><b>Zach Leatherman</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/issues?q=author%3Azachleat" title="Bug reports">🐛</a></td>
|
||||
<td align="center"><a href="https://github.com/thardy"><img src="https://avatars3.githubusercontent.com/u/120636?v=4" width="100px;" alt=""/><br /><sub><b>Tim Hardy</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=thardy" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://paulrobertlloyd.com/"><img src="https://avatars3.githubusercontent.com/u/813383?v=4" width="100px;" alt=""/><br /><sub><b>Paul Robert Lloyd</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=paulrobertlloyd" title="Code">💻</a> <a href="https://github.com/harttle/liquidjs/issues?q=author%3Apaulrobertlloyd" title="Bug reports">🐛</a></td>
|
||||
<td align="center"><a href="https://twitter.com/alecdotbiz"><img src="https://avatars2.githubusercontent.com/u/1925840?v=4" width="100px;" alt=""/><br /><sub><b>Alec Larson</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=aleclarson" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/pmalouin"><img src="https://avatars1.githubusercontent.com/u/1411117?v=4" width="100px;" alt=""/><br /><sub><b>Patrick Malouin</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=pmalouin" title="Code">💻</a> <a href="https://github.com/harttle/liquidjs/commits?author=pmalouin" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://jaswrks.com"><img src="https://avatars3.githubusercontent.com/u/1563559?v=4" width="100px;" alt="jaswrks"/><br /><sub><b>jaswrks</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=jaswrks" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://oott123.com"><img src="https://avatars2.githubusercontent.com/u/905663?v=4" width="100px;" alt="三三"/><br /><sub><b>三三</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=oott123" title="Code">💻</a> <a href="#ideas-oott123" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center"><a href="https://github.com/ssendev"><img src="https://avatars0.githubusercontent.com/u/450793?v=4" width="100px;" alt="ssendev"/><br /><sub><b>ssendev</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=ssendev" title="Code">💻</a> <a href="https://github.com/harttle/liquidjs/commits?author=ssendev" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/wojtask9"><img src="https://avatars3.githubusercontent.com/u/6099236?v=4" width="100px;" alt="wojtask9"/><br /><sub><b>wojtask9</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=wojtask9" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/thelornenelson"><img src="https://avatars3.githubusercontent.com/u/24596583?v=4" width="100px;" alt="Andrew Barclay"/><br /><sub><b>Andrew Barclay</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=thelornenelson" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://www.stam.pr/"><img src="https://avatars2.githubusercontent.com/u/142338?v=4" width="100px;" alt="Cory Mawhorter"/><br /><sub><b>Cory Mawhorter</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=cmawhorter" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/thehappybug"><img src="https://avatars0.githubusercontent.com/u/3393530?v=4" width="100px;" alt="Mehdi Jaffery"/><br /><sub><b>Mehdi Jaffery</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=thehappybug" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://jaswrks.com"><img src="https://avatars3.githubusercontent.com/u/1563559?v=4" width="100px;" alt=""/><br /><sub><b>jaswrks</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=jaswrks" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://oott123.com"><img src="https://avatars2.githubusercontent.com/u/905663?v=4" width="100px;" alt=""/><br /><sub><b>三三</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=oott123" title="Code">💻</a> <a href="#ideas-oott123" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center"><a href="https://github.com/ssendev"><img src="https://avatars0.githubusercontent.com/u/450793?v=4" width="100px;" alt=""/><br /><sub><b>ssendev</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=ssendev" title="Code">💻</a> <a href="https://github.com/harttle/liquidjs/commits?author=ssendev" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/wojtask9"><img src="https://avatars3.githubusercontent.com/u/6099236?v=4" width="100px;" alt=""/><br /><sub><b>wojtask9</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=wojtask9" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/thelornenelson"><img src="https://avatars3.githubusercontent.com/u/24596583?v=4" width="100px;" alt=""/><br /><sub><b>Andrew Barclay</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=thelornenelson" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://www.stam.pr/"><img src="https://avatars2.githubusercontent.com/u/142338?v=4" width="100px;" alt=""/><br /><sub><b>Cory Mawhorter</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=cmawhorter" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/thehappybug"><img src="https://avatars0.githubusercontent.com/u/3393530?v=4" width="100px;" alt=""/><br /><sub><b>Mehdi Jaffery</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=thehappybug" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/robinbijlani"><img src="https://avatars0.githubusercontent.com/u/2503108?v=4" width="100px;" alt="Robin Bijlani"/><br /><sub><b>Robin Bijlani</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=robinbijlani" title="Code">💻</a> <a href="https://github.com/harttle/liquidjs/issues?q=author%3Arobinbijlani" title="Bug reports">🐛</a></td>
|
||||
<td align="center"><a href="https://www.rmkennedy.com"><img src="https://avatars3.githubusercontent.com/u/8356669?v=4" width="100px;" alt="Ryan Kennedy"/><br /><sub><b>Ryan Kennedy</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=ryaninvents" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/strax"><img src="https://avatars2.githubusercontent.com/u/587213?v=4" width="100px;" alt="Sami Kukkonen"/><br /><sub><b>Sami Kukkonen</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=strax" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://ScottFreeCode.github.io/"><img src="https://avatars3.githubusercontent.com/u/16506071?v=4" width="100px;" alt="Scott Santucci"/><br /><sub><b>Scott Santucci</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=ScottFreeCode" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://stevenrescigno.com"><img src="https://avatars3.githubusercontent.com/u/8505293?v=4" width="100px;" alt="Steven "/><br /><sub><b>Steven </b></sub></a><br /><a href="#example-stevenanthonyrevo" title="Examples">💡</a> <a href="https://github.com/harttle/liquidjs/commits?author=stevenanthonyrevo" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://efcl.info/"><img src="https://avatars1.githubusercontent.com/u/19714?v=4" width="100px;" alt="azu"/><br /><sub><b>azu</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=azu" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/wyozi"><img src="https://avatars3.githubusercontent.com/u/4894573?v=4" width="100px;" alt="Joonas"/><br /><sub><b>Joonas</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=wyozi" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/robinbijlani"><img src="https://avatars0.githubusercontent.com/u/2503108?v=4" width="100px;" alt=""/><br /><sub><b>Robin Bijlani</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=robinbijlani" title="Code">💻</a> <a href="https://github.com/harttle/liquidjs/issues?q=author%3Arobinbijlani" title="Bug reports">🐛</a></td>
|
||||
<td align="center"><a href="https://www.rmkennedy.com"><img src="https://avatars3.githubusercontent.com/u/8356669?v=4" width="100px;" alt=""/><br /><sub><b>Ryan Kennedy</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=ryaninvents" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/strax"><img src="https://avatars2.githubusercontent.com/u/587213?v=4" width="100px;" alt=""/><br /><sub><b>Sami Kukkonen</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=strax" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://ScottFreeCode.github.io/"><img src="https://avatars3.githubusercontent.com/u/16506071?v=4" width="100px;" alt=""/><br /><sub><b>Scott Santucci</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=ScottFreeCode" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://stevenrescigno.com"><img src="https://avatars3.githubusercontent.com/u/8505293?v=4" width="100px;" alt=""/><br /><sub><b>Steven </b></sub></a><br /><a href="#example-stevenanthonyrevo" title="Examples">💡</a> <a href="https://github.com/harttle/liquidjs/commits?author=stevenanthonyrevo" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://efcl.info/"><img src="https://avatars1.githubusercontent.com/u/19714?v=4" width="100px;" alt=""/><br /><sub><b>azu</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=azu" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/wyozi"><img src="https://avatars3.githubusercontent.com/u/4894573?v=4" width="100px;" alt=""/><br /><sub><b>Joonas</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=wyozi" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/jamelait"><img src="https://avatars1.githubusercontent.com/u/14369255?v=4" width="100px;" alt="Jamel A."/><br /><sub><b>Jamel A.</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=jamelait" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://brandonpittman.net"><img src="https://avatars0.githubusercontent.com/u/967145?v=4" width="100px;" alt="Brandon Pittman"/><br /><sub><b>Brandon Pittman</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=brandonpittman" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/tgrandgent"><img src="https://avatars3.githubusercontent.com/u/17069042?v=4" width="100px;" alt="tgrandgent"/><br /><sub><b>tgrandgent</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=tgrandgent" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/mastodon0"><img src="https://avatars1.githubusercontent.com/u/7924332?v=4" width="100px;" alt="Martin Schuster"/><br /><sub><b>Martin Schuster</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=mastodon0" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://js.chenlei.me"><img src="https://avatars0.githubusercontent.com/u/6339390?v=4" width="100px;" alt="Ray"/><br /><sub><b>Ray</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=richardo2016" title="Tests">⚠️</a> <a href="https://github.com/harttle/liquidjs/commits?author=richardo2016" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/CriGoT"><img src="https://avatars0.githubusercontent.com/u/1936786?v=4" width="100px;" alt="Cristofer Gonzales"/><br /><sub><b>Cristofer Gonzales</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=CriGoT" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://www.raymondcamden.com"><img src="https://avatars3.githubusercontent.com/u/393660?v=4" width="100px;" alt="Raymond Camden"/><br /><sub><b>Raymond Camden</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=cfjedimaster" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/jamelait"><img src="https://avatars1.githubusercontent.com/u/14369255?v=4" width="100px;" alt=""/><br /><sub><b>Jamel A.</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=jamelait" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://brandonpittman.net"><img src="https://avatars0.githubusercontent.com/u/967145?v=4" width="100px;" alt=""/><br /><sub><b>Brandon Pittman</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=brandonpittman" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/tgrandgent"><img src="https://avatars3.githubusercontent.com/u/17069042?v=4" width="100px;" alt=""/><br /><sub><b>tgrandgent</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=tgrandgent" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/mastodon0"><img src="https://avatars1.githubusercontent.com/u/7924332?v=4" width="100px;" alt=""/><br /><sub><b>Martin Schuster</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=mastodon0" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://js.chenlei.me"><img src="https://avatars0.githubusercontent.com/u/6339390?v=4" width="100px;" alt=""/><br /><sub><b>Ray</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=richardo2016" title="Tests">⚠️</a> <a href="https://github.com/harttle/liquidjs/commits?author=richardo2016" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/CriGoT"><img src="https://avatars0.githubusercontent.com/u/1936786?v=4" width="100px;" alt=""/><br /><sub><b>Cristofer Gonzales</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=CriGoT" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://www.raymondcamden.com"><img src="https://avatars3.githubusercontent.com/u/393660?v=4" width="100px;" alt=""/><br /><sub><b>Raymond Camden</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=cfjedimaster" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://stedman.dev"><img src="https://avatars1.githubusercontent.com/u/183122?v=4" width="100px;" alt="Steve Stedman"/><br /><sub><b>Steve Stedman</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=stedman" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://ciccarello.me"><img src="https://avatars0.githubusercontent.com/u/11273838?v=4" width="100px;" alt="Anthony Ciccarello"/><br /><sub><b>Anthony Ciccarello</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=aciccarello" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://stedman.dev"><img src="https://avatars1.githubusercontent.com/u/183122?v=4" width="100px;" alt=""/><br /><sub><b>Steve Stedman</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=stedman" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://ciccarello.me"><img src="https://avatars0.githubusercontent.com/u/11273838?v=4" width="100px;" alt=""/><br /><sub><b>Anthony Ciccarello</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=aciccarello" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://twitter.com/IAmTrySound"><img src="https://avatars0.githubusercontent.com/u/5635476?v=4" width="100px;" alt=""/><br /><sub><b>Bogdan Chadkin</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=TrySound" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://hightouch.io"><img src="https://avatars0.githubusercontent.com/u/5959235?v=4" width="100px;" alt=""/><br /><sub><b>Tejas Manohar</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=tejasmanohar" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://about.me/peterdehaan"><img src="https://avatars2.githubusercontent.com/u/557895?v=4" width="100px;" alt=""/><br /><sub><b>Peter deHaan</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=pdehaan" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/amit777"><img src="https://avatars0.githubusercontent.com/u/2703309?v=4" width="100px;" alt=""/><br /><sub><b>amit777</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=amit777" title="Code">💻</a> <a href="#financial-amit777" title="Financial">💵</a></td>
|
||||
<td align="center"><a href="http://www.ifi.uzh.ch/en/ce/people/schuldenzucker.html"><img src="https://avatars3.githubusercontent.com/u/1100776?v=4" width="100px;" alt=""/><br /><sub><b>Steffen Schuldenzucker</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=sschuldenzucker" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/Pixcell"><img src="https://avatars0.githubusercontent.com/u/4005291?v=4" width="100px;" alt=""/><br /><sub><b>Pixcell</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=Pixcell" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Only the latest major version is supported with security updates. It can be changed if many people are relying on a specific version for some reason. If this is the case, welcome to file an issue.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please contact yangjvn@126.com to report a vulnerability.
|
||||
|
||||
- It will be fixed very soon (maybe within 1 week or couple of days) if that vulnerability obviously affects common use cases.
|
||||
- Otherwise, it'll be scheduled with a priority just like requested features (which is lower than bugs).
|
||||
- If it's declined, you'll receive an email (more oftenly there will be a discuss).
|
||||
+9
-1
@@ -9,8 +9,16 @@ npm run build
|
||||
cp CNAME public/
|
||||
|
||||
cp ../CHANGELOG.md source/tutorials/changelog.md
|
||||
sed -i \
|
||||
-e 's/{%/{% raw %}{%{% endraw %}/g' \
|
||||
-e 's/{{/{% raw %}{{{% endraw %}/g' \
|
||||
-e '1 s/"/\"/g' \
|
||||
-e '1 s/</\</g' \
|
||||
-e '1 s/>/\>/g' \
|
||||
source/tutorials/changelog.md
|
||||
cp source/tutorials/changelog.md source/zh-cn/tutorials/changelog.md
|
||||
|
||||
sed -i '1i ---\ntitle: Changelog\nauto: true\n---\n' source/tutorials/changelog.md
|
||||
cp ../CHANGELOG.md source/zh-cn/tutorials/changelog.md
|
||||
sed -i '1i ---\ntitle: 更新日志\nauto: true\n---\n' source/zh-cn/tutorials/changelog.md
|
||||
|
||||
if [ "$HEXO_ALGOLIA_INDEXING_KEY" != "" ]; then
|
||||
|
||||
@@ -4,7 +4,7 @@ const isQuote = c => c === '"' || c === "'"
|
||||
const isOperator = c => '!=<>'.includes(c)
|
||||
const isNumber = c => c >= '0' && c <= '9'
|
||||
const isCharacter = c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
|
||||
const isVariable = c => '_-?'.includes(c) || isCharacter(c) || isNumber(c)
|
||||
const isIdentifier = c => '_-?'.includes(c) || isCharacter(c) || isNumber(c)
|
||||
const isBlank = c => c === '\n' || c === '\t' || c === ' ' || c === '\r' || c === '\v' || c === '\f'
|
||||
const isInlineBlank = c => c === '\t' || c === ' ' || c === '\r'
|
||||
const isSign = c => c === '-' || c === '+'
|
||||
@@ -15,7 +15,7 @@ const types = []
|
||||
for (let i = 0; i < 128; i++) {
|
||||
const c = String.fromCharCode(i)
|
||||
let n = 0
|
||||
if (isVariable(c)) n |= 1
|
||||
if (isIdentifier(c)) n |= 1
|
||||
if (isOperator(c)) n |= 2
|
||||
if (isBlank(c)) n |= 4
|
||||
if (isQuote(c)) n |= 8
|
||||
@@ -31,7 +31,7 @@ console.log(`
|
||||
// This file is generated by bin/character-gen.js
|
||||
// bitmask character types to boost performance
|
||||
export const TYPES = [${types.join(', ')}]
|
||||
export const VARIABLE = 1
|
||||
export const IDENTIFIER = 1
|
||||
export const OPERATOR = 2
|
||||
export const BLANK = 4
|
||||
export const QUOTE = 8
|
||||
|
||||
Generated
+9
-9
@@ -2127,9 +2127,9 @@
|
||||
}
|
||||
},
|
||||
"highlight.js": {
|
||||
"version": "9.18.1",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.18.1.tgz",
|
||||
"integrity": "sha512-OrVKYz70LHsnCgmbXctv/bfuvntIKDz177h0Co37DQ5jamGZLVmoCVMtjMtNZY3X9DrCcKfklHPNeA0uPZhSJg=="
|
||||
"version": "9.18.5",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.18.5.tgz",
|
||||
"integrity": "sha512-a5bFyofd/BHCX52/8i8uJkjr9DYwXIPnM/plwI6W7ezItLGqzt7X2G2nXuYSfsIJdkwwj/g9DG1LkcGJI/dDoA=="
|
||||
},
|
||||
"html-entities": {
|
||||
"version": "1.2.1",
|
||||
@@ -2528,9 +2528,9 @@
|
||||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.15",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
|
||||
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
|
||||
"version": "4.17.19",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
|
||||
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
|
||||
},
|
||||
"lodash.assignin": {
|
||||
"version": "4.2.0",
|
||||
@@ -3060,9 +3060,9 @@
|
||||
"integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE="
|
||||
},
|
||||
"prismjs": {
|
||||
"version": "1.19.0",
|
||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.19.0.tgz",
|
||||
"integrity": "sha512-IVFtbW9mCWm9eOIaEkNyo2Vl4NnEifis2GQ7/MLRG5TQe6t+4Sj9J5QWI9i3v+SS43uZBlCAOn+zYTVYQcPXJw==",
|
||||
"version": "1.21.0",
|
||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.21.0.tgz",
|
||||
"integrity": "sha512-uGdSIu1nk3kej2iZsLyDoJ7e9bnPzIgY0naW/HdknGj61zScaprVEVGHrPoXqI+M9sP0NDnTK2jpkvmldpuqDw==",
|
||||
"requires": {
|
||||
"clipboard": "^2.0.0"
|
||||
}
|
||||
|
||||
@@ -65,6 +65,12 @@ Before 2.0.1, <code>extname</code> is set to `.liquid` by default. To change tha
|
||||
|
||||
**globals** is used to define global variables available to all templates even in cases of [render tag][render]. See [3185][185] for details.
|
||||
|
||||
## jsTruthy
|
||||
|
||||
**jsTruthy** is used to use standard Javascript truthiness rather than the Shopify.
|
||||
|
||||
it defaults to false. For example, when set to true, a blank string would evaluate to false with jsTruthy. With Shopify's truthiness, a blank string is true.
|
||||
|
||||
## Trimming
|
||||
|
||||
**greedy**, **trimOutputLeft**, **trimOutputRight**, **trimTagLeft**, **trimTagRight** options are used to eliminate extra newlines and indents in templates arround Liquid Constructs. See [Whitespace Control][wc] for details.
|
||||
@@ -83,6 +89,8 @@ Before 2.0.1, <code>extname</code> is set to `.liquid` by default. To change tha
|
||||
|
||||
**strictVariables** is used to assert variable existence. If set to `false`, undefined variables will be rendered as empty string. Otherwise, undefined variables will cause a render exception. Defaults to `false`.
|
||||
|
||||
**lenientIf** modifies the behavior of `strictVariables` to allow handling optional variables. If set to `true`, an undefined variable will *not* cause an exception in the following two situations: a) it is the condition to an `if`, `elsif`, or `unless` tag; b) it occurs right before a `default` filter. Irrelevant if `strictVariables` is not set. Defaults to `false`.
|
||||
|
||||
{% note info Non-existent Tags %}
|
||||
Non-existent tags always throw errors during pasrsing and this behaviour can not be customized.
|
||||
{% endnote %}
|
||||
|
||||
@@ -42,12 +42,12 @@ Pre-built UMD bundles are also available:
|
||||
|
||||
```html
|
||||
<!--for production-->
|
||||
<script src="https://cdn.jsdelivr.net/npm/liquidjs/dist/liquid.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/liquidjs/dist/liquid.browser.min.js"></script>
|
||||
<!--for development-->
|
||||
<script src="https://cdn.jsdelivr.net/npm/liquidjs/dist/liquid.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/liquidjs/dist/liquid.browser.umd.js"></script>
|
||||
```
|
||||
|
||||
{% note info Working Demo %} Here's a living demo on jsFiddle: <a href="https://jsfiddle.net/x43eb0z6/" target="_blank">jsfiddle.net/x43eb0z6</a>, and the source code is also available in <a href="https://github.com/harttle/liquidjs/blob/master/demo/browser/" target="_blank">liquidjs/demo/browser/</a>.{% endnote %}
|
||||
{% note info Working Demo %} Here's a living demo on jsFiddle: <a href="https://jsfiddle.net/pd4jhzLs/1/" target="_blank">jsfiddle.net/pd4jhzLs/1/</a>, and the source code is also available in <a href="https://github.com/harttle/liquidjs/blob/master/demo/browser/" target="_blank">liquidjs/demo/browser/</a>.{% endnote %}
|
||||
|
||||
{% note warn Compatibility %} You may need a <a href="https://github.com/taylorhakes/promise-polyfill" target="_blank">Promise polyfill</a> for legacy browsers like IE and Android UC, see <a href="http://caniuse.com/#feat=promises" target="_blank">caniuse statistics</a>. {% endnote %}
|
||||
|
||||
@@ -69,4 +69,4 @@ echo 'Hello, {{ name }}.' | npx liquidjs '{"name": "Snake"}'
|
||||
|
||||
A ReactJS demo is also added by [@stevenanthonyrevo](https://github.com/stevenanthonyrevo), see [liquidjs/demo/reactjs/](https://github.com/harttle/liquidjs/blob/master/demo/reactjs/).
|
||||
|
||||
[intro]: ./intro-to-liquid.html
|
||||
[intro]: ./intro-to-liquid.html
|
||||
|
||||
@@ -17,11 +17,30 @@ value | truthy | falsy
|
||||
`string` | ✔️ |
|
||||
`empty string` | ✔️ |
|
||||
`0` | ✔️ |
|
||||
`integer` | ✔️ |
|
||||
`integer` | ✔️ |
|
||||
`float` | ✔️ |
|
||||
`array` | ✔️ |
|
||||
`empty array` | ✔️ |
|
||||
|
||||
## Use JavaScript Truthy
|
||||
|
||||
Note that liquidjs use Shopify's truthiness by default. But it can be toggled to used standard JavaScript truthiness by setting the **jsTruthy** option to `true`.
|
||||
|
||||
value | truthy | falsy
|
||||
--- | --- | ---
|
||||
`true` | ✔️ |
|
||||
`false` | | ✔️
|
||||
`null` | | ✔️
|
||||
`undefined` | | ✔️
|
||||
`string` | ✔️ |
|
||||
`empty string` | | ✔️
|
||||
`0` | | ✔️
|
||||
`integer` | ✔️ |
|
||||
`float` | ✔️ |
|
||||
`array` | ✔️ |
|
||||
`empty array` | ✔️ |
|
||||
|
||||
|
||||
[ruby]: https://shopify.github.io/liquid
|
||||
[sl]: https://www.npmjs.com/package/liquidjs
|
||||
[diff]: https://github.com/harttle/liquidjs#differences-and-limitations
|
||||
@@ -63,6 +63,12 @@ LiquidJS 把这个选项默认值设为 <code>true</code> 以兼容于 shopify/l
|
||||
|
||||
**globals** 用来定义对所有模板可见的全局变量。包括 [render tag][render] 引入的子模板,见 [3185][185]。
|
||||
|
||||
## jsTruthy
|
||||
|
||||
**jsTruthy** 用来使用 Javascript 的真值判断,默认为 `false` 使用 Shopify 方式。
|
||||
|
||||
例如,空字符串在 JavaScript 中为假(`jsTruthy` 为 `true` 时),在 Shopify 真值表中为真。
|
||||
|
||||
## 换行和缩进
|
||||
|
||||
**greedy**, **trimOutputLeft**, **trimOutputRight**, **trimTagLeft**, **trimTagRight** 选项用来移除 Liquid 语法周围的换行和缩进,详情请参考 [Whitespace Control][wc]。
|
||||
|
||||
@@ -8,7 +8,7 @@ title: 真和假
|
||||
|
||||
根据 [Shopify 的文档](https://shopify.github.io/liquid/basics/truthy-and-falsy/),Ruby 版本除了 `false` 和 `nil` 之外的所有值都是真,但 JavaScript 有完全不同的类型系统,比如我们有 `undefined` 类型,以及不区分 `integer` 和 `float`,因此有些不同:
|
||||
|
||||
value | truthy | falsy
|
||||
值 | 真 | 假
|
||||
--- | --- | ---
|
||||
`true` | ✔️ |
|
||||
`false` | | ✔️
|
||||
@@ -22,6 +22,24 @@ value | truthy | falsy
|
||||
`array` | ✔️ |
|
||||
`empty array` | ✔️ |
|
||||
|
||||
## 使用 JavaScript 真值
|
||||
|
||||
liquidjs 默认使用 Shopify 的真值表,但可以通过设置 **jsTruthy** 选项为 `true` 来使用标准的 JavaScript 真值。
|
||||
|
||||
值 | 真 | 假
|
||||
--- | --- | ---
|
||||
`true` | ✔️ |
|
||||
`false` | | ✔️
|
||||
`null` | | ✔️
|
||||
`undefined` | | ✔️
|
||||
`string` | ✔️ |
|
||||
`empty string` | | ✔️
|
||||
`0` | | ✔️
|
||||
`integer` | ✔️ |
|
||||
`float` | ✔️ |
|
||||
`array` | ✔️ |
|
||||
`empty array` | ✔️ |
|
||||
|
||||
[ruby]: https://shopify.github.io/liquid
|
||||
[sl]: https://www.npmjs.com/package/liquidjs
|
||||
[diff]: https://github.com/harttle/liquidjs#differences-and-limitations
|
||||
Vendored
+3
-3
@@ -28,9 +28,9 @@
|
||||
<ul id="intro-cmd-wrap">
|
||||
<li class="intro-code-item"><span class="hl">import</span> { Liquid } <span class="hl">from</span> 'liquidjs'</li>
|
||||
<li class="intro-code-item"><span class="hl">const</span> engine = <span class="hl">new</span> Liquid()</li>
|
||||
<li class="intro-code-item"><span class="hl">const</span> tpl = engine.parse(<span class="hl-str">'Welcome to {{v}}!'</span>))</li>
|
||||
<li class="intro-code-item"><span class="hl">const</span> html = <span class="hl">await</span> engine.render(tpl, {v: "Liquid"})</li>
|
||||
<li class="intro-code-item"><span class="hl-comment">// html === "Welcome to Liquid!"</span></li>
|
||||
<li class="intro-code-item"><span class="hl">const</span> tpl = engine.parse(<span class="hl-str">'Welcome to {{v}}!'</span>)</li>
|
||||
<li class="intro-code-item"><span class="hl">const</span> engine.render(tpl, {v: "Liquid"}).then(console.log)</li>
|
||||
<li class="intro-code-item"><span class="hl-comment">// Output: "Welcome to Liquid!"</span></li>
|
||||
</ul>
|
||||
<div id="intro-get-started-wrap">
|
||||
<a href="tutorials/setup.html" id="intro-get-started-link">{{ __('index.get_started') }}</a>
|
||||
|
||||
+2
-4
@@ -50,10 +50,8 @@
|
||||
background: color-link-hover
|
||||
|
||||
#banner-share
|
||||
display: none
|
||||
display: block
|
||||
padding-bottom: 60px
|
||||
@media mq-normal
|
||||
display: block
|
||||
|
||||
#intro-news-list
|
||||
background-color: darken(color-navy, 10%)
|
||||
@@ -178,7 +176,7 @@
|
||||
transition: 0.2s
|
||||
color: color-link
|
||||
&:hover
|
||||
background: @border-color
|
||||
background: lighten(color-link, 20%)
|
||||
color: #fff
|
||||
|
||||
#sponsors-wrap, #contributors-wrap
|
||||
|
||||
Vendored
+17
-2
@@ -66,9 +66,8 @@
|
||||
preview.renderer.setShowGutter(false);
|
||||
preview.renderer.setPadding(20);
|
||||
|
||||
editor.setValue(sourceEl.textContent, 1);
|
||||
editor.setValue(getInitialValue(), 1);
|
||||
editor.on('change', update);
|
||||
editor.focus();
|
||||
update();
|
||||
|
||||
function createEditor(id, lang) {
|
||||
@@ -83,8 +82,16 @@
|
||||
return editor;
|
||||
}
|
||||
|
||||
function getInitialValue() {
|
||||
try {
|
||||
return atou(location.hash.slice(1)) || sourceEl.textContent
|
||||
} catch (e) {}
|
||||
return sourceEl.textContent
|
||||
}
|
||||
|
||||
async function update() {
|
||||
const tpl = editor.getValue();
|
||||
history.replaceState({}, '', '#' + utoa(tpl));
|
||||
try {
|
||||
const html = await engine.parseAndRender(tpl);
|
||||
preview.setValue(html, 1);
|
||||
@@ -93,6 +100,14 @@
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function atou(str) {
|
||||
return decodeURIComponent(escape(atob(str)))
|
||||
}
|
||||
|
||||
function utoa(str) {
|
||||
return btoa(unescape(encodeURIComponent(str)))
|
||||
}
|
||||
}());
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
|
||||
Generated
+4991
-398
File diff suppressed because it is too large
Load Diff
+11
-7
@@ -1,13 +1,17 @@
|
||||
{
|
||||
"name": "liquidjs",
|
||||
"version": "9.13.0",
|
||||
"version": "9.19.0",
|
||||
"description": "A simple, expressive, safe and Shopify compatible template engine in pure JavaScript.",
|
||||
"main": "dist/liquid.cjs.js",
|
||||
"es2015": "dist/liquid.esm.js",
|
||||
"browser": "dist/liquid.js",
|
||||
"main": "dist/liquid.node.cjs.js",
|
||||
"module": "dist/liquid.node.esm.js",
|
||||
"es2015": "dist/liquid.browser.esm.js",
|
||||
"browser": {
|
||||
"./dist/liquid.node.cjs.js": "./dist/liquid.browser.umd.js",
|
||||
"./dist/liquid.node.esm.js": "./dist/liquid.browser.esm.js"
|
||||
},
|
||||
"types": "dist/liquid.d.ts",
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"lint": "eslint '**/*.ts' .",
|
||||
"check": "npm test && npm run lint",
|
||||
"unit": "mocha \"test/unit/**/*.ts\"",
|
||||
"integration": "mocha \"test/integration/**/*.ts\"",
|
||||
@@ -16,7 +20,7 @@
|
||||
"benchmark": "node --expose-gc benchmark/index",
|
||||
"coverage": "nyc --reporter=html mocha \"test/{unit,integration}/**/*.ts\"",
|
||||
"coverage-coveralls": "nyc mocha \"test/{unit,integration}/**/*.ts\" && nyc report --reporter=text-lcov | coveralls",
|
||||
"build": "rollup -c rollup.config.ts && ls -lh dist",
|
||||
"build": "rm -rf dist && rollup -c rollup.config.ts && ls -lh dist",
|
||||
"build-docs": "bin/build-docs.sh",
|
||||
"watch": "tsc --watch"
|
||||
},
|
||||
@@ -96,7 +100,7 @@
|
||||
"rollup-plugin-replace": "^2.1.0",
|
||||
"rollup-plugin-typescript2": "^0.21.1",
|
||||
"rollup-plugin-uglify": "^6.0.2",
|
||||
"semantic-release": "^15.13.19",
|
||||
"semantic-release": "^17.2.3",
|
||||
"sinon": "^7.5.0",
|
||||
"sinon-chai": "^3.3.0",
|
||||
"supertest": "^3.4.2",
|
||||
|
||||
+34
-13
@@ -15,9 +15,9 @@ const treeshake = {
|
||||
}
|
||||
const input = './src/liquid.ts'
|
||||
|
||||
const cjs = {
|
||||
const nodeCjs = {
|
||||
output: [{
|
||||
file: 'dist/liquid.cjs.js',
|
||||
file: 'dist/liquid.node.cjs.js',
|
||||
format: 'cjs',
|
||||
banner
|
||||
}],
|
||||
@@ -36,9 +36,30 @@ const cjs = {
|
||||
input
|
||||
}
|
||||
|
||||
const esm = {
|
||||
const nodeEsm = {
|
||||
output: [{
|
||||
file: 'dist/liquid.esm.js',
|
||||
file: 'dist/liquid.node.esm.js',
|
||||
format: 'esm',
|
||||
banner
|
||||
}],
|
||||
external: ['path', 'fs'],
|
||||
plugins: [typescript({
|
||||
tsconfigOverride: {
|
||||
include: [ 'src' ],
|
||||
exclude: [ 'test', 'benchmark' ],
|
||||
compilerOptions: {
|
||||
target: 'ES2017',
|
||||
module: 'ES2015'
|
||||
}
|
||||
}
|
||||
})],
|
||||
treeshake,
|
||||
input
|
||||
}
|
||||
|
||||
const browserEsm = {
|
||||
output: [{
|
||||
file: 'dist/liquid.browser.esm.js',
|
||||
format: 'esm',
|
||||
banner
|
||||
}],
|
||||
@@ -64,9 +85,9 @@ const esm = {
|
||||
input
|
||||
}
|
||||
|
||||
const umd = {
|
||||
const browserUmd = {
|
||||
output: [{
|
||||
file: 'dist/liquid.js',
|
||||
file: 'dist/liquid.browser.umd.js',
|
||||
name: 'liquidjs',
|
||||
format: 'umd',
|
||||
sourcemap,
|
||||
@@ -93,9 +114,9 @@ const umd = {
|
||||
input
|
||||
}
|
||||
|
||||
const min = {
|
||||
const browserMin = {
|
||||
output: [{
|
||||
file: 'dist/liquid.min.js',
|
||||
file: 'dist/liquid.browser.min.js',
|
||||
name: 'liquidjs',
|
||||
format: 'umd',
|
||||
sourcemap
|
||||
@@ -124,10 +145,10 @@ const min = {
|
||||
|
||||
const bundles = []
|
||||
const env = process.env.BUNDLES || ''
|
||||
if (env.includes('cjs')) bundles.push(cjs)
|
||||
if (env.includes('esm')) bundles.push(esm)
|
||||
if (env.includes('umd')) bundles.push(umd)
|
||||
if (env.includes('min')) bundles.push(min)
|
||||
if (bundles.length === 0) bundles.push(cjs, umd, min, esm)
|
||||
if (env.includes('cjs')) bundles.push(nodeCjs)
|
||||
if (env.includes('esm')) bundles.push(nodeEsm, browserEsm)
|
||||
if (env.includes('umd')) bundles.push(browserUmd)
|
||||
if (env.includes('min')) bundles.push(browserMin)
|
||||
if (bundles.length === 0) bundles.push(nodeCjs, nodeEsm, browserEsm, browserUmd, browserMin)
|
||||
|
||||
export default bundles
|
||||
|
||||
@@ -36,7 +36,7 @@ export function slice<T> (v: T[], begin: number, length = 1): T[] {
|
||||
export function where<T extends object> (this: FilterImpl, arr: T[], property: string, expected?: any): T[] {
|
||||
return toArray(arr).filter(obj => {
|
||||
const value = this.context.getFromScope(obj, String(property).split('.'))
|
||||
return expected === undefined ? isTruthy(value) : value === expected
|
||||
return expected === undefined ? isTruthy(value, this.context) : value === expected
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import strftime from '../../util/strftime'
|
||||
import strftime, { TimezoneDate } from '../../util/strftime'
|
||||
import { isString, isNumber } from '../../util/underscore'
|
||||
import { FilterImpl } from '../../template/filter/filter-impl'
|
||||
|
||||
export function date (v: string | Date, arg: string) {
|
||||
export function date (this: FilterImpl, v: string | Date, arg: string) {
|
||||
let date = v
|
||||
if (v === 'now' || v === 'today') {
|
||||
date = new Date()
|
||||
} else if (isNumber(v)) {
|
||||
date = new Date(v * 1000)
|
||||
} else if (isString(v)) {
|
||||
date = /^\d+$/.test(v) ? new Date(+v * 1000) : new Date(v)
|
||||
if (/^\d+$/.test(v)) {
|
||||
date = new Date(+v * 1000)
|
||||
} else if (this.context.opts.preserveTimezones) {
|
||||
date = new TimezoneDate(v)
|
||||
} else {
|
||||
date = new Date(v)
|
||||
}
|
||||
}
|
||||
return isValidDate(date) ? strftime(date, arg) : v
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { isFalsy } from '../../render/boolean'
|
||||
import { isArray, isString, toValue } from '../../util/underscore'
|
||||
import { FilterImpl } from '../../template/filter/filter-impl'
|
||||
|
||||
export function Default<T1, T2> (v: string | T1, arg: T2): string | T1 | T2 {
|
||||
export function Default<T1, T2> (this: FilterImpl, v: string | T1, arg: T2): string | T1 | T2 {
|
||||
if (isArray(v) || isString(v)) return v.length ? v : arg
|
||||
return isFalsy(toValue(v)) ? arg : v
|
||||
return isFalsy(toValue(v), this.context) ? arg : v
|
||||
}
|
||||
export function json (v: any) {
|
||||
return JSON.stringify(v)
|
||||
|
||||
@@ -29,11 +29,11 @@ export function upcase (str: string) {
|
||||
}
|
||||
|
||||
export function remove (v: string, arg: string) {
|
||||
return stringify(v).split(arg).join('')
|
||||
return stringify(v).split(String(arg)).join('')
|
||||
}
|
||||
|
||||
export function removeFirst (v: string, l: string) {
|
||||
return stringify(v).replace(l, '')
|
||||
return stringify(v).replace(String(l), '')
|
||||
}
|
||||
|
||||
export function rstrip (str: string) {
|
||||
@@ -41,7 +41,7 @@ export function rstrip (str: string) {
|
||||
}
|
||||
|
||||
export function split (v: string, arg: string) {
|
||||
return stringify(v).split(arg)
|
||||
return stringify(v).split(String(arg))
|
||||
}
|
||||
|
||||
export function strip (v: string) {
|
||||
@@ -58,11 +58,11 @@ export function capitalize (str: string) {
|
||||
}
|
||||
|
||||
export function replace (v: string, pattern: string, replacement: string) {
|
||||
return stringify(v).split(pattern).join(replacement)
|
||||
return stringify(v).split(String(pattern)).join(replacement)
|
||||
}
|
||||
|
||||
export function replaceFirst (v: string, arg1: string, arg2: string) {
|
||||
return stringify(v).replace(arg1, arg2)
|
||||
return stringify(v).replace(String(arg1), arg2)
|
||||
}
|
||||
|
||||
export function truncate (v: string, l = 50, o = '...') {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Tokenizer, assert, TagImplOptions, TagToken, Context } from '../../type
|
||||
export default {
|
||||
parse: function (token: TagToken) {
|
||||
const tokenizer = new Tokenizer(token.args)
|
||||
this.key = tokenizer.readWord().content
|
||||
this.key = tokenizer.readIdentifier().content
|
||||
tokenizer.skipBlank()
|
||||
assert(tokenizer.peek() === '=', () => `illegal token ${token.getText()}`)
|
||||
tokenizer.advance()
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Tokenizer, assert, Template, Context, TagImplOptions, TagToken, TopLevelToken } from '../../types'
|
||||
import { evalQuotedToken } from '../../render/expression'
|
||||
|
||||
export default {
|
||||
parse: function (tagToken: TagToken, remainTokens: TopLevelToken[]) {
|
||||
const tokenizer = new Tokenizer(tagToken.args)
|
||||
this.variable = tokenizer.readWord().content
|
||||
this.variable = readVariableName(tokenizer)
|
||||
assert(this.variable, () => `${tagToken.args} not valid identifier`)
|
||||
|
||||
this.templates = []
|
||||
@@ -22,3 +23,10 @@ export default {
|
||||
ctx.bottom()[this.variable] = html
|
||||
}
|
||||
} as TagImplOptions
|
||||
|
||||
function readVariableName (tokenizer: Tokenizer) {
|
||||
const word = tokenizer.readIdentifier().content
|
||||
if (word) return word
|
||||
const quoted = tokenizer.readQuoted()
|
||||
if (quoted) return evalQuotedToken(quoted)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { isNumber, stringify } from '../../util/underscore'
|
||||
export default {
|
||||
parse: function (token: TagToken) {
|
||||
const tokenizer = new Tokenizer(token.args)
|
||||
this.variable = tokenizer.readWord().content
|
||||
this.variable = tokenizer.readIdentifier().content
|
||||
},
|
||||
render: function (context: Context, emitter: Emitter) {
|
||||
const scope = context.environments
|
||||
|
||||
@@ -8,8 +8,8 @@ export default {
|
||||
parse: function (token: TagToken, remainTokens: TopLevelToken[]) {
|
||||
const toknenizer = new Tokenizer(token.args)
|
||||
|
||||
const variable = toknenizer.readWord()
|
||||
const inStr = toknenizer.readWord()
|
||||
const variable = toknenizer.readIdentifier()
|
||||
const inStr = toknenizer.readIdentifier()
|
||||
const collection = toknenizer.readValue()
|
||||
assert(
|
||||
variable.size() && inStr.content === 'in' && collection,
|
||||
@@ -36,7 +36,7 @@ export default {
|
||||
},
|
||||
render: function * (ctx: Context, emitter: Emitter) {
|
||||
const r = this.liquid.renderer
|
||||
let collection = toEnumerable(evalToken(this.collection, ctx))
|
||||
let collection = toEnumerable(yield evalToken(this.collection, ctx))
|
||||
|
||||
if (!collection.length) {
|
||||
yield r.renderTemplates(this.elseTemplates, ctx, emitter)
|
||||
|
||||
@@ -31,8 +31,8 @@ export default {
|
||||
const r = this.liquid.renderer
|
||||
|
||||
for (const branch of this.branches) {
|
||||
const cond = yield new Expression(branch.cond).value(ctx)
|
||||
if (isTruthy(cond)) {
|
||||
const cond = yield new Expression(branch.cond, ctx.opts.lenientIf).value(ctx)
|
||||
if (isTruthy(cond, ctx)) {
|
||||
yield r.renderTemplates(branch.templates, ctx, emitter)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export default {
|
||||
assert(this.file, () => `illegal argument "${token.args}"`)
|
||||
|
||||
const begin = tokenizer.p
|
||||
const withStr = tokenizer.readWord()
|
||||
const withStr = tokenizer.readIdentifier()
|
||||
if (withStr.content === 'with') {
|
||||
tokenizer.skipBlank()
|
||||
if (tokenizer.peek() !== ':') {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Tokenizer, Emitter, TagToken, Context, TagImplOptions } from '../../typ
|
||||
export default {
|
||||
parse: function (token: TagToken) {
|
||||
const tokenizer = new Tokenizer(token.args)
|
||||
this.variable = tokenizer.readWord().content
|
||||
this.variable = tokenizer.readIdentifier().content
|
||||
},
|
||||
render: function (context: Context, emitter: Emitter) {
|
||||
const scope = context.environments
|
||||
|
||||
@@ -15,16 +15,16 @@ export default {
|
||||
while (!tokenizer.end()) {
|
||||
tokenizer.skipBlank()
|
||||
const begin = tokenizer.p
|
||||
const keyword = tokenizer.readWord()
|
||||
const keyword = tokenizer.readIdentifier()
|
||||
if (keyword.content === 'with' || keyword.content === 'for') {
|
||||
tokenizer.skipBlank()
|
||||
if (tokenizer.peek() !== ':') {
|
||||
const value = tokenizer.readValue()
|
||||
if (value) {
|
||||
const beforeAs = tokenizer.p
|
||||
const asStr = tokenizer.readWord()
|
||||
const asStr = tokenizer.readIdentifier()
|
||||
let alias
|
||||
if (asStr.content === 'as') alias = tokenizer.readWord()
|
||||
if (asStr.content === 'as') alias = tokenizer.readIdentifier()
|
||||
else tokenizer.p = beforeAs
|
||||
|
||||
this[keyword.content] = { value, alias: alias && alias.content }
|
||||
|
||||
@@ -7,10 +7,10 @@ export default {
|
||||
parse: function (tagToken: TagToken, remainTokens: TopLevelToken[]) {
|
||||
const tokenizer = new Tokenizer(tagToken.args)
|
||||
|
||||
this.variable = tokenizer.readWord()
|
||||
this.variable = tokenizer.readIdentifier()
|
||||
tokenizer.skipBlank()
|
||||
|
||||
const tmp = tokenizer.readWord()
|
||||
const tmp = tokenizer.readIdentifier()
|
||||
assert(tmp && tmp.content === 'in', () => `illegal tag: ${tagToken.getText()}`)
|
||||
|
||||
this.collection = tokenizer.readValue()
|
||||
@@ -30,7 +30,7 @@ export default {
|
||||
},
|
||||
|
||||
render: function * (ctx: Context, emitter: Emitter) {
|
||||
let collection = toEnumerable(evalToken(this.collection, ctx))
|
||||
let collection = toEnumerable(yield evalToken(this.collection, ctx))
|
||||
const hash = yield this.hash.render(ctx)
|
||||
const offset = hash.offset || 0
|
||||
const limit = (hash.limit === undefined) ? collection.length : hash.limit
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { TopLevelToken, Template, Emitter, Expression, isFalsy, ParseStream, Context, TagImplOptions, Token, TagToken } from '../../types'
|
||||
import { TopLevelToken, Template, Emitter, Expression, isTruthy, isFalsy, ParseStream, Context, TagImplOptions, TagToken } from '../../types'
|
||||
|
||||
export default {
|
||||
parse: function (tagToken: TagToken, remainTokens: TopLevelToken[]) {
|
||||
this.templates = []
|
||||
this.branches = []
|
||||
this.elseTemplates = []
|
||||
let p
|
||||
const stream: ParseStream = this.liquid.parser.parseStream(remainTokens)
|
||||
@@ -10,6 +11,12 @@ export default {
|
||||
p = this.templates
|
||||
this.cond = tagToken.args
|
||||
})
|
||||
.on('tag:elsif', (token: TagToken) => {
|
||||
this.branches.push({
|
||||
cond: token.args,
|
||||
templates: p = []
|
||||
})
|
||||
})
|
||||
.on('tag:else', () => (p = this.elseTemplates))
|
||||
.on('tag:endunless', () => stream.stop())
|
||||
.on('template', (tpl: Template) => p.push(tpl))
|
||||
@@ -22,9 +29,21 @@ export default {
|
||||
|
||||
render: function * (ctx: Context, emitter: Emitter) {
|
||||
const r = this.liquid.renderer
|
||||
const cond = yield new Expression(this.cond).value(ctx)
|
||||
yield (isFalsy(cond)
|
||||
? r.renderTemplates(this.templates, ctx, emitter)
|
||||
: r.renderTemplates(this.elseTemplates, ctx, emitter))
|
||||
const cond = yield new Expression(this.cond, ctx.opts.lenientIf).value(ctx)
|
||||
|
||||
if (isFalsy(cond, ctx)) {
|
||||
yield r.renderTemplates(this.templates, ctx, emitter)
|
||||
return
|
||||
}
|
||||
|
||||
for (const branch of this.branches) {
|
||||
const cond = yield new Expression(branch.cond, ctx.opts.lenientIf).value(ctx)
|
||||
if (isTruthy(cond, ctx)) {
|
||||
yield r.renderTemplates(branch.templates, ctx, emitter)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
yield r.renderTemplates(this.elseTemplates, ctx, emitter)
|
||||
}
|
||||
} as TagImplOptions
|
||||
|
||||
@@ -3,6 +3,7 @@ import { __assign } from 'tslib'
|
||||
import { NormalizedFullOptions, defaultOptions } from '../liquid-options'
|
||||
import { Scope } from './scope'
|
||||
import { isArray, isNil, isString, isFunction, toLiquid } from '../util/underscore'
|
||||
import { InternalUndefinedVariableError } from '../util/error'
|
||||
|
||||
export class Context {
|
||||
private scopes: Scope[] = [{}]
|
||||
@@ -42,7 +43,7 @@ export class Context {
|
||||
return paths.reduce((scope, path) => {
|
||||
scope = readProperty(scope, path)
|
||||
if (isNil(scope) && this.opts.strictVariables) {
|
||||
throw new TypeError(`undefined variable: ${path}`)
|
||||
throw new InternalUndefinedVariableError(path)
|
||||
}
|
||||
return scope
|
||||
}, scope)
|
||||
|
||||
+17
-1
@@ -11,12 +11,16 @@ export interface LiquidOptions {
|
||||
extname?: string;
|
||||
/** Whether or not to cache resolved templates. Defaults to `false`. */
|
||||
cache?: boolean | number | Cache<Template[]>;
|
||||
/** Use Javascript Truthiness. Defaults to `false`. */
|
||||
jsTruthy?: boolean;
|
||||
/** If set, treat the `filepath` parameter in `{%include filepath %}` and `{%layout filepath%}` as a variable, otherwise as a literal value. Defaults to `true`. */
|
||||
dynamicPartials?: boolean;
|
||||
/** Whether or not to assert filter existence. If set to `false`, undefined filters will be skipped. Otherwise, undefined filters will cause an exception. Defaults to `false`. */
|
||||
strictFilters?: boolean;
|
||||
/** Whether or not to assert variable existence. If set to `false`, undefined variables will be rendered as empty string. Otherwise, undefined variables will cause an exception. Defaults to `false`. */
|
||||
strictVariables?: boolean;
|
||||
/** Modifies the behavior of `strictVariables`. If set, a single undefined variable will *not* cause an exception in the context of the `if`/`elsif`/`unless` tag and the `default` filter. Instead, it will evaluate to `false` and `null`, respectively. Irrelevant if `strictVariables` is not set. Defaults to `false`. **/
|
||||
lenientIf?: boolean;
|
||||
/** Strip blank characters (including ` `, `\t`, and `\r`) from the right of tags (`{% %}`) until `\n` (inclusive). Defaults to `false`. */
|
||||
trimTagRight?: boolean;
|
||||
/** Similar to `trimTagRight`, whereas the `\n` is exclusive. Defaults to `false`. See Whitespace Control for details. */
|
||||
@@ -33,12 +37,16 @@ export interface LiquidOptions {
|
||||
outputDelimiterLeft?: string;
|
||||
/** The right delimiter for liquid outputs. **/
|
||||
outputDelimiterRight?: string;
|
||||
/** Whether input strings to date filter preserve the given timezone **/
|
||||
preserveTimezones?: boolean;
|
||||
/** Whether `trim*Left`/`trim*Right` is greedy. When set to `true`, all consecutive blank characters including `\n` will be trimed regardless of line breaks. Defaults to `true`. */
|
||||
greedy?: boolean;
|
||||
/** `fs` is used to override the default file-system module with a custom implementation. */
|
||||
fs?: FS;
|
||||
/** the global environment passed down to all partial templates, i.e. templates included by `include`, `layout` and `render` tags. */
|
||||
globals?: object;
|
||||
/** Whether or not to keep value type when writing the Output. Defaults to `false`. */
|
||||
keepOutputType?: boolean;
|
||||
}
|
||||
|
||||
interface NormalizedOptions extends LiquidOptions {
|
||||
@@ -50,9 +58,11 @@ export interface NormalizedFullOptions extends NormalizedOptions {
|
||||
root: string[];
|
||||
extname: string;
|
||||
cache: undefined | Cache<Template[]>;
|
||||
jsTruthy: boolean;
|
||||
dynamicPartials: boolean;
|
||||
strictFilters: boolean;
|
||||
strictVariables: boolean;
|
||||
lenientIf: boolean;
|
||||
trimTagRight: boolean;
|
||||
trimTagLeft: boolean;
|
||||
trimOutputRight: boolean;
|
||||
@@ -61,8 +71,10 @@ export interface NormalizedFullOptions extends NormalizedOptions {
|
||||
tagDelimiterRight: string;
|
||||
outputDelimiterLeft: string;
|
||||
outputDelimiterRight: string;
|
||||
preserveTimezones: boolean;
|
||||
greedy: boolean;
|
||||
globals: object;
|
||||
keepOutputType: boolean;
|
||||
}
|
||||
|
||||
export const defaultOptions: NormalizedFullOptions = {
|
||||
@@ -70,6 +82,7 @@ export const defaultOptions: NormalizedFullOptions = {
|
||||
cache: undefined,
|
||||
extname: '',
|
||||
dynamicPartials: true,
|
||||
jsTruthy: false,
|
||||
trimTagRight: false,
|
||||
trimTagLeft: false,
|
||||
trimOutputRight: false,
|
||||
@@ -79,9 +92,12 @@ export const defaultOptions: NormalizedFullOptions = {
|
||||
tagDelimiterRight: '%}',
|
||||
outputDelimiterLeft: '{{',
|
||||
outputDelimiterRight: '}}',
|
||||
preserveTimezones: false,
|
||||
strictFilters: false,
|
||||
strictVariables: false,
|
||||
globals: {}
|
||||
lenientIf: false,
|
||||
globals: {},
|
||||
keepOutputType: false
|
||||
}
|
||||
|
||||
export function normalize (options?: LiquidOptions): NormalizedOptions {
|
||||
|
||||
+16
-14
@@ -14,7 +14,8 @@ import { FilterMap } from './template/filter/filter-map'
|
||||
import { LiquidOptions, normalizeStringArray, NormalizedFullOptions, applyDefault, normalize } from './liquid-options'
|
||||
import { FilterImplOptions } from './template/filter/filter-impl-options'
|
||||
import { FS } from './fs/fs'
|
||||
import { toThenable, toValue } from './util/async'
|
||||
import { toPromise, toValue } from './util/async'
|
||||
import { Emitter } from './render/emitter'
|
||||
|
||||
export * from './types'
|
||||
|
||||
@@ -31,7 +32,7 @@ export class Liquid {
|
||||
this.parser = new Parser(this)
|
||||
this.renderer = new Render()
|
||||
this.fs = opts.fs || fs
|
||||
this.filters = new FilterMap(this.options.strictFilters)
|
||||
this.filters = new FilterMap(this.options.strictFilters, this)
|
||||
this.tags = new TagMap()
|
||||
|
||||
forOwn(builtinTags, (conf: TagImplOptions, name: string) => this.registerTag(snakeCase(name), conf))
|
||||
@@ -43,26 +44,27 @@ export class Liquid {
|
||||
return this.parser.parse(tokens)
|
||||
}
|
||||
|
||||
public _render (tpl: Template[], scope?: object, opts?: LiquidOptions, sync?: boolean): IterableIterator<string> {
|
||||
public _render (tpl: Template[], scope?: object, opts?: LiquidOptions, sync?: boolean): IterableIterator<any> {
|
||||
const options = { ...this.options, ...normalize(opts) }
|
||||
const ctx = new Context(scope, options, sync)
|
||||
return this.renderer.renderTemplates(tpl, ctx)
|
||||
const emitter = new Emitter(options.keepOutputType)
|
||||
return this.renderer.renderTemplates(tpl, ctx, emitter)
|
||||
}
|
||||
public async render (tpl: Template[], scope?: object, opts?: LiquidOptions): Promise<string> {
|
||||
return toThenable(this._render(tpl, scope, opts, false))
|
||||
public async render (tpl: Template[], scope?: object, opts?: LiquidOptions): Promise<any> {
|
||||
return toPromise(this._render(tpl, scope, opts, false))
|
||||
}
|
||||
public renderSync (tpl: Template[], scope?: object, opts?: LiquidOptions): string {
|
||||
public renderSync (tpl: Template[], scope?: object, opts?: LiquidOptions): any {
|
||||
return toValue(this._render(tpl, scope, opts, true))
|
||||
}
|
||||
|
||||
public _parseAndRender (html: string, scope?: object, opts?: LiquidOptions, sync?: boolean): IterableIterator<string> {
|
||||
public _parseAndRender (html: string, scope?: object, opts?: LiquidOptions, sync?: boolean): IterableIterator<any> {
|
||||
const tpl = this.parse(html)
|
||||
return this._render(tpl, scope, opts, sync)
|
||||
}
|
||||
public async parseAndRender (html: string, scope?: object, opts?: LiquidOptions): Promise<string> {
|
||||
return toThenable(this._parseAndRender(html, scope, opts, false))
|
||||
public async parseAndRender (html: string, scope?: object, opts?: LiquidOptions): Promise<any> {
|
||||
return toPromise(this._parseAndRender(html, scope, opts, false))
|
||||
}
|
||||
public parseAndRenderSync (html: string, scope?: object, opts?: LiquidOptions): string {
|
||||
public parseAndRenderSync (html: string, scope?: object, opts?: LiquidOptions): any {
|
||||
return toValue(this._parseAndRender(html, scope, opts, true))
|
||||
}
|
||||
|
||||
@@ -88,7 +90,7 @@ export class Liquid {
|
||||
throw this.lookupError(file, options.root)
|
||||
}
|
||||
public async parseFile (file: string, opts?: LiquidOptions): Promise<Template[]> {
|
||||
return toThenable(this._parseFile(file, opts, false))
|
||||
return toPromise(this._parseFile(file, opts, false))
|
||||
}
|
||||
public parseFileSync (file: string, opts?: LiquidOptions): Template[] {
|
||||
return toValue(this._parseFile(file, opts, true))
|
||||
@@ -104,11 +106,11 @@ export class Liquid {
|
||||
}
|
||||
|
||||
public _evalValue (str: string, ctx: Context): IterableIterator<any> {
|
||||
const value = new Value(str, this.filters)
|
||||
const value = new Value(str, this.filters, this)
|
||||
return value.value(ctx)
|
||||
}
|
||||
public async evalValue (str: string, ctx: Context): Promise<any> {
|
||||
return toThenable(this._evalValue(str, ctx))
|
||||
return toPromise(this._evalValue(str, ctx))
|
||||
}
|
||||
public evalValueSync (str: string, ctx: Context): any {
|
||||
return toValue(this._evalValue(str, ctx))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { VARIABLE } from '../util/character'
|
||||
import { IDENTIFIER } from '../util/character'
|
||||
|
||||
const trie = {
|
||||
a: { n: { d: { end: true, needBoundary: true } } },
|
||||
@@ -19,6 +19,6 @@ export function matchOperator (str: string, begin: number, end = str.length) {
|
||||
if (node['end']) info = node
|
||||
}
|
||||
if (!info) return -1
|
||||
if (info['needBoundary'] && str.charCodeAt(i) & VARIABLE) return -1
|
||||
if (info['needBoundary'] && str.charCodeAt(i) & IDENTIFIER) return -1
|
||||
return i
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export default class Parser {
|
||||
return new Tag(token, remainTokens, this.liquid)
|
||||
}
|
||||
if (isOutputToken(token)) {
|
||||
return new Output(token as OutputToken, this.liquid.filters)
|
||||
return new Output(token as OutputToken, this.liquid.filters, this.liquid)
|
||||
}
|
||||
return new HTML(token)
|
||||
} catch (e) {
|
||||
|
||||
+76
-27
@@ -1,6 +1,6 @@
|
||||
import { whiteSpaceCtrl } from './whitespace-ctrl'
|
||||
import { NumberToken } from '../tokens/number-token'
|
||||
import { WordToken } from '../tokens/word-token'
|
||||
import { IdentifierToken } from '../tokens/identifier-token'
|
||||
import { literalValues } from '../util/literal'
|
||||
import { LiteralToken } from '../tokens/literal-token'
|
||||
import { OperatorToken } from '../tokens/operator-token'
|
||||
@@ -20,12 +20,14 @@ import { ValueToken } from '../tokens/value-token'
|
||||
import { OutputToken } from '../tokens/output-token'
|
||||
import { TokenizationError } from '../util/error'
|
||||
import { NormalizedFullOptions, defaultOptions } from '../liquid-options'
|
||||
import { TYPES, QUOTE, BLANK, VARIABLE } from '../util/character'
|
||||
import { TYPES, QUOTE, BLANK, IDENTIFIER } from '../util/character'
|
||||
import { matchOperator } from './match-operator'
|
||||
|
||||
export class Tokenizer {
|
||||
p = 0
|
||||
N: number
|
||||
private rawBeginAt = -1
|
||||
|
||||
constructor (
|
||||
private input: string,
|
||||
private file: string = ''
|
||||
@@ -70,7 +72,7 @@ export class Tokenizer {
|
||||
assert(this.peek() === '|', () => `unexpected token at ${this.snapshot()}`)
|
||||
this.p++
|
||||
const begin = this.p
|
||||
const name = this.readWord()
|
||||
const name = this.readIdentifier()
|
||||
if (!name.size()) return null
|
||||
const args = []
|
||||
this.skipBlank()
|
||||
@@ -107,8 +109,9 @@ export class Tokenizer {
|
||||
|
||||
readTopLevelToken (options: NormalizedFullOptions): TopLevelToken {
|
||||
const { tagDelimiterLeft, outputDelimiterLeft } = options
|
||||
if (this.matchWord(tagDelimiterLeft)) return this.readTagToken(options)
|
||||
if (this.matchWord(outputDelimiterLeft)) return this.readOutputToken(options)
|
||||
if (this.rawBeginAt > -1) return this.readEndrawOrRawContent(options)
|
||||
if (this.match(tagDelimiterLeft)) return this.readTagToken(options)
|
||||
if (this.match(outputDelimiterLeft)) return this.readOutputToken(options)
|
||||
return this.readHTMLToken(options)
|
||||
}
|
||||
|
||||
@@ -116,8 +119,8 @@ export class Tokenizer {
|
||||
const begin = this.p
|
||||
while (this.p < this.N) {
|
||||
const { tagDelimiterLeft, outputDelimiterLeft } = options
|
||||
if (this.matchWord(tagDelimiterLeft)) break
|
||||
if (this.matchWord(outputDelimiterLeft)) break
|
||||
if (this.match(tagDelimiterLeft)) break
|
||||
if (this.match(outputDelimiterLeft)) break
|
||||
++this.p
|
||||
}
|
||||
return new HTMLToken(this.input, begin, this.p, this.file)
|
||||
@@ -128,9 +131,11 @@ export class Tokenizer {
|
||||
const { tagDelimiterRight } = options
|
||||
const begin = this.p
|
||||
if (this.readTo(tagDelimiterRight) === -1) {
|
||||
this.mkError(`tag ${this.snapshot(begin)} not closed`, begin)
|
||||
throw this.mkError(`tag ${this.snapshot(begin)} not closed`, begin)
|
||||
}
|
||||
return new TagToken(input, begin, this.p, options, file)
|
||||
const token = new TagToken(input, begin, this.p, options, file)
|
||||
if (token.name === 'raw') this.rawBeginAt = begin
|
||||
return token
|
||||
}
|
||||
|
||||
readOutputToken (options: NormalizedFullOptions): OutputToken {
|
||||
@@ -138,24 +143,59 @@ export class Tokenizer {
|
||||
const { outputDelimiterRight } = options
|
||||
const begin = this.p
|
||||
if (this.readTo(outputDelimiterRight) === -1) {
|
||||
this.mkError(`output ${this.snapshot(begin)} not closed`, begin)
|
||||
throw this.mkError(`output ${this.snapshot(begin)} not closed`, begin)
|
||||
}
|
||||
return new OutputToken(input, begin, this.p, options, file)
|
||||
}
|
||||
|
||||
readEndrawOrRawContent (options: NormalizedFullOptions): HTMLToken | TagToken {
|
||||
const { tagDelimiterLeft, tagDelimiterRight } = options
|
||||
const begin = this.p
|
||||
let leftPos = this.readTo(tagDelimiterLeft) - tagDelimiterLeft.length
|
||||
while (this.p < this.N) {
|
||||
if (this.readIdentifier().getText() !== 'endraw') {
|
||||
leftPos = this.readTo(tagDelimiterLeft) - tagDelimiterLeft.length
|
||||
continue
|
||||
}
|
||||
while (this.p <= this.N) {
|
||||
if (this.rmatch(tagDelimiterRight)) {
|
||||
const end = this.p
|
||||
if (begin === leftPos) {
|
||||
this.rawBeginAt = -1
|
||||
return new TagToken(this.input, begin, end, options, this.file)
|
||||
} else {
|
||||
this.p = leftPos
|
||||
return new HTMLToken(this.input, begin, leftPos, this.file)
|
||||
}
|
||||
}
|
||||
if (this.rmatch(tagDelimiterLeft)) break
|
||||
this.p++
|
||||
}
|
||||
}
|
||||
throw this.mkError(`raw ${this.snapshot(this.rawBeginAt)} not closed`, begin)
|
||||
}
|
||||
|
||||
mkError (msg: string, begin: number) {
|
||||
throw new TokenizationError(msg, new WordToken(this.input, begin, this.N, this.file))
|
||||
return new TokenizationError(msg, new IdentifierToken(this.input, begin, this.N, this.file))
|
||||
}
|
||||
|
||||
snapshot (begin: number = this.p) {
|
||||
return JSON.stringify(ellipsis(this.input.slice(begin), 16))
|
||||
}
|
||||
|
||||
readWord (): WordToken { // rename to identifier
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
readWord () {
|
||||
console.warn('Tokenizer#readWord() will be removed, use #readIdentifier instead')
|
||||
return this.readIdentifier()
|
||||
}
|
||||
|
||||
readIdentifier (): IdentifierToken {
|
||||
this.skipBlank()
|
||||
const begin = this.p
|
||||
while (this.peekType() & VARIABLE) ++this.p
|
||||
return new WordToken(this.input, begin, this.p, this.file)
|
||||
while (this.peekType() & IDENTIFIER) ++this.p
|
||||
return new IdentifierToken(this.input, begin, this.p, this.file)
|
||||
}
|
||||
|
||||
readHashes () {
|
||||
@@ -171,7 +211,7 @@ export class Tokenizer {
|
||||
this.skipBlank()
|
||||
if (this.peek() === ',') ++this.p
|
||||
const begin = this.p
|
||||
const name = this.readWord()
|
||||
const name = this.readIdentifier()
|
||||
if (!name.size()) return
|
||||
let value
|
||||
|
||||
@@ -198,7 +238,7 @@ export class Tokenizer {
|
||||
readTo (end: string): number {
|
||||
while (this.p < this.N) {
|
||||
++this.p
|
||||
if (this.reverseMatchWord(end)) return this.p
|
||||
if (this.rmatch(end)) return this.p
|
||||
}
|
||||
return -1
|
||||
}
|
||||
@@ -207,21 +247,30 @@ export class Tokenizer {
|
||||
const value = this.readQuoted() || this.readRange()
|
||||
if (value) return value
|
||||
|
||||
const variable = this.readWord()
|
||||
if (this.peek() === '[') {
|
||||
this.p++
|
||||
const prop = this.readQuoted()
|
||||
if (!prop) return
|
||||
if (this.peek() !== ']') return
|
||||
this.p++
|
||||
return new PropertyAccessToken(prop, [], this.p)
|
||||
}
|
||||
|
||||
const variable = this.readIdentifier()
|
||||
if (!variable.size()) return
|
||||
|
||||
let isNumber = variable.isNumber(true)
|
||||
const props: (QuotedToken | WordToken)[] = []
|
||||
const props: (QuotedToken | IdentifierToken)[] = []
|
||||
while (true) {
|
||||
if (this.peek() === '[') {
|
||||
isNumber = false
|
||||
this.p++
|
||||
const prop = this.readValue() || new WordToken(this.input, this.p, this.p, this.file)
|
||||
const prop = this.readValue() || new IdentifierToken(this.input, this.p, this.p, this.file)
|
||||
this.readTo(']')
|
||||
props.push(prop)
|
||||
} else if (this.peek() === '.' && this.peek(1) !== '.') { // skip range syntax
|
||||
this.p++
|
||||
const prop = this.readWord()
|
||||
const prop = this.readIdentifier()
|
||||
if (!prop.size()) break
|
||||
if (!prop.isNumber()) isNumber = false
|
||||
props.push(prop)
|
||||
@@ -230,7 +279,7 @@ export class Tokenizer {
|
||||
if (!props.length && literalValues.hasOwnProperty(variable.content)) {
|
||||
return new LiteralToken(this.input, variable.begin, variable.end, this.file)
|
||||
}
|
||||
if (isNumber) return new NumberToken(variable, props[0] as WordToken)
|
||||
if (isNumber) return new NumberToken(variable, props[0] as IdentifierToken)
|
||||
return new PropertyAccessToken(variable, props, this.p)
|
||||
}
|
||||
|
||||
@@ -267,22 +316,22 @@ export class Tokenizer {
|
||||
return new QuotedToken(this.input, begin, this.p, this.file)
|
||||
}
|
||||
|
||||
readFileName (): WordToken {
|
||||
readFileName (): IdentifierToken {
|
||||
const begin = this.p
|
||||
while (!(this.peekType() & BLANK) && this.peek() !== ',' && this.p < this.N) this.p++
|
||||
return new WordToken(this.input, begin, this.p, this.file)
|
||||
return new IdentifierToken(this.input, begin, this.p, this.file)
|
||||
}
|
||||
|
||||
matchWord (word: string) {
|
||||
match (word: string) {
|
||||
for (let i = 0; i < word.length; i++) {
|
||||
if (word[i] !== this.input[this.p + i]) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
reverseMatchWord (word: string) {
|
||||
for (let i = 0; i < word.length; i++) {
|
||||
if (word[word.length - 1 - i] !== this.input[this.p - 1 - i]) return false
|
||||
rmatch (pattern: string) {
|
||||
for (let i = 0; i < pattern.length; i++) {
|
||||
if (pattern[pattern.length - 1 - i] !== this.input[this.p - 1 - i]) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { NormalizedFullOptions } from '../liquid-options'
|
||||
import { TYPES, INLINE_BLANK, BLANK } from '../util/character'
|
||||
|
||||
export function whiteSpaceCtrl (tokens: Token[], options: NormalizedFullOptions) {
|
||||
options = { greedy: true, ...options }
|
||||
let inRaw = false
|
||||
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
|
||||
+11
-4
@@ -1,6 +1,13 @@
|
||||
export function isTruthy (val: any): boolean {
|
||||
return !isFalsy(val)
|
||||
import { Context } from '../context/context'
|
||||
|
||||
export function isTruthy (val: any, ctx: Context): boolean {
|
||||
return !isFalsy(val, ctx)
|
||||
}
|
||||
export function isFalsy (val: any): boolean {
|
||||
return val === false || undefined === val || val === null
|
||||
|
||||
export function isFalsy (val: any, ctx: Context): boolean {
|
||||
if (ctx.opts.jsTruthy) {
|
||||
return !val
|
||||
} else {
|
||||
return val === false || undefined === val || val === null
|
||||
}
|
||||
}
|
||||
|
||||
+23
-3
@@ -1,9 +1,29 @@
|
||||
import { stringify, toValue } from '../util/underscore'
|
||||
|
||||
export class Emitter {
|
||||
public html = '';
|
||||
public html: any = '';
|
||||
public break = false;
|
||||
public continue = false;
|
||||
private keepOutputType? = false;
|
||||
|
||||
public write (html: string) {
|
||||
this.html += html
|
||||
constructor (keepOutputType: boolean|undefined) {
|
||||
this.keepOutputType = keepOutputType
|
||||
}
|
||||
|
||||
public write (html: any) {
|
||||
if (this.keepOutputType === true) {
|
||||
html = toValue(html)
|
||||
} else {
|
||||
html = stringify(toValue(html))
|
||||
}
|
||||
// This will only preserve the type if the value is isolated.
|
||||
// I.E:
|
||||
// {{ my-port }} -> 42
|
||||
// {{ my-host }}:{{ my-port }} -> 'host:42'
|
||||
if (this.keepOutputType === true && typeof html !== 'string' && this.html === '') {
|
||||
this.html = html
|
||||
} else {
|
||||
this.html = stringify(this.html) + stringify(html)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+21
-10
@@ -12,24 +12,27 @@ import { Context } from '../context/context'
|
||||
import { range, toValue } from '../util/underscore'
|
||||
import { Tokenizer } from '../parser/tokenizer'
|
||||
import { operatorImpls } from '../render/operator'
|
||||
import { UndefinedVariableError, InternalUndefinedVariableError } from '../util/error'
|
||||
|
||||
export class Expression {
|
||||
private operands: any[] = []
|
||||
private postfix: IterableIterator<Token>
|
||||
private postfix: Token[]
|
||||
private lenient: boolean
|
||||
|
||||
public constructor (str: string) {
|
||||
public constructor (str: string, lenient = false) {
|
||||
const tokenizer = new Tokenizer(str)
|
||||
this.postfix = toPostfix(tokenizer.readExpression())
|
||||
this.postfix = [...toPostfix(tokenizer.readExpression())]
|
||||
this.lenient = lenient
|
||||
}
|
||||
public evaluate (ctx: Context): any {
|
||||
for (const token of this.postfix) {
|
||||
if (TypeGuards.isOperatorToken(token)) {
|
||||
const r = this.operands.pop()
|
||||
const l = this.operands.pop()
|
||||
const result = evalOperatorToken(token, l, r)
|
||||
const result = evalOperatorToken(token, l, r, ctx)
|
||||
this.operands.push(result)
|
||||
} else {
|
||||
this.operands.push(evalToken(token, ctx))
|
||||
this.operands.push(evalToken(token, ctx, this.lenient && this.postfix.length === 1))
|
||||
}
|
||||
}
|
||||
return this.operands[0]
|
||||
@@ -39,12 +42,20 @@ export class Expression {
|
||||
}
|
||||
}
|
||||
|
||||
export function evalToken (token: Token | undefined, ctx: Context): any {
|
||||
export function evalToken (token: Token | undefined, ctx: Context, lenient = false): any {
|
||||
assert(ctx, () => 'unable to evaluate: context not defined')
|
||||
if (TypeGuards.isPropertyAccessToken(token)) {
|
||||
const variable = token.variable.getText()
|
||||
const variable = token.getVariableAsText()
|
||||
const props: string[] = token.props.map(prop => evalToken(prop, ctx))
|
||||
return ctx.get([variable, ...props])
|
||||
try {
|
||||
return ctx.get([variable, ...props])
|
||||
} catch (e) {
|
||||
if (lenient && e instanceof InternalUndefinedVariableError) {
|
||||
return null
|
||||
} else {
|
||||
throw (new UndefinedVariableError(e, token))
|
||||
}
|
||||
}
|
||||
}
|
||||
if (TypeGuards.isRangeToken(token)) return evalRangeToken(token, ctx)
|
||||
if (TypeGuards.isLiteralToken(token)) return evalLiteralToken(token)
|
||||
@@ -62,9 +73,9 @@ export function evalQuotedToken (token: QuotedToken) {
|
||||
return parseStringLiteral(token.getText())
|
||||
}
|
||||
|
||||
function evalOperatorToken (token: OperatorToken, lhs: any, rhs: any) {
|
||||
function evalOperatorToken (token: OperatorToken, lhs: any, rhs: any, ctx: Context) {
|
||||
const impl = operatorImpls[token.operator]
|
||||
return impl(lhs, rhs)
|
||||
return impl(lhs, rhs, ctx)
|
||||
}
|
||||
|
||||
function evalLiteralToken (token: LiteralToken) {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { isComparable } from '../drop/comparable'
|
||||
import { Context } from '../context/context'
|
||||
import { isFunction } from '../util/underscore'
|
||||
import { isTruthy } from '../render/boolean'
|
||||
|
||||
export const operatorImpls: {[key: string]: (lhs: any, rhs: any) => boolean} = {
|
||||
export const operatorImpls: {[key: string]: (lhs: any, rhs: any, ctx: Context) => boolean} = {
|
||||
'==': (l: any, r: any) => {
|
||||
if (isComparable(l)) return l.equals(r)
|
||||
if (isComparable(r)) return r.equals(l)
|
||||
@@ -36,6 +37,6 @@ export const operatorImpls: {[key: string]: (lhs: any, rhs: any) => boolean} = {
|
||||
'contains': (l: any, r: any) => {
|
||||
return l && isFunction(l.indexOf) ? l.indexOf(r) > -1 : false
|
||||
},
|
||||
'and': (l: any, r: any) => isTruthy(l) && isTruthy(r),
|
||||
'or': (l: any, r: any) => isTruthy(l) || isTruthy(r)
|
||||
'and': (l: any, r: any, ctx: Context) => isTruthy(l, ctx) && isTruthy(r, ctx),
|
||||
'or': (l: any, r: any, ctx: Context) => isTruthy(l, ctx) || isTruthy(r, ctx)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@ import { Template } from '../template/template'
|
||||
import { Emitter } from './emitter'
|
||||
|
||||
export class Render {
|
||||
public * renderTemplates (templates: Template[], ctx: Context, emitter = new Emitter()): IterableIterator<string> {
|
||||
public * renderTemplates (templates: Template[], ctx: Context, emitter?: Emitter): IterableIterator<any> {
|
||||
if (!emitter) {
|
||||
emitter = new Emitter(ctx.opts.keepOutputType)
|
||||
}
|
||||
for (const tpl of templates) {
|
||||
try {
|
||||
const html = yield tpl.render(ctx, emitter)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Context } from '../../context/context'
|
||||
import { Liquid } from '../../liquid'
|
||||
|
||||
export interface FilterImpl {
|
||||
context: Context;
|
||||
liquid: Liquid;
|
||||
}
|
||||
|
||||
@@ -2,11 +2,15 @@ import { FilterImplOptions } from './filter-impl-options'
|
||||
import { Filter } from './filter'
|
||||
import { FilterArg } from '../../parser/filter-arg'
|
||||
import { assert } from '../../util/assert'
|
||||
import { Liquid } from '../../liquid'
|
||||
|
||||
export class FilterMap {
|
||||
private impls: {[key: string]: FilterImplOptions} = {}
|
||||
|
||||
constructor (private readonly strictFilters: boolean) {}
|
||||
constructor (
|
||||
private readonly strictFilters: boolean,
|
||||
private readonly liquid: Liquid
|
||||
) {}
|
||||
|
||||
get (name: string) {
|
||||
const impl = this.impls[name]
|
||||
@@ -19,6 +23,6 @@ export class FilterMap {
|
||||
}
|
||||
|
||||
create (name: string, args: FilterArg[]) {
|
||||
return new Filter(name, this.get(name), args)
|
||||
return new Filter(name, this.get(name), args, this.liquid)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,16 +3,19 @@ import { Context } from '../../context/context'
|
||||
import { identify } from '../../util/underscore'
|
||||
import { FilterImplOptions } from './filter-impl-options'
|
||||
import { FilterArg, isKeyValuePair } from '../../parser/filter-arg'
|
||||
import { Liquid } from '../../liquid'
|
||||
|
||||
export class Filter {
|
||||
public name: string
|
||||
public args: FilterArg[]
|
||||
private impl: FilterImplOptions
|
||||
private liquid: Liquid
|
||||
|
||||
public constructor (name: string, impl: FilterImplOptions, args: FilterArg[]) {
|
||||
public constructor (name: string, impl: FilterImplOptions, args: FilterArg[], liquid: Liquid) {
|
||||
this.name = name
|
||||
this.impl = impl || identify
|
||||
this.args = args
|
||||
this.liquid = liquid
|
||||
}
|
||||
public * render (value: any, context: Context) {
|
||||
const argv: any[] = []
|
||||
@@ -20,6 +23,6 @@ export class Filter {
|
||||
if (isKeyValuePair(arg)) argv.push([arg[0], yield evalToken(arg[1], context)])
|
||||
else argv.push(yield evalToken(arg, context))
|
||||
}
|
||||
return yield this.impl.apply({ context }, [value, ...argv])
|
||||
return yield this.impl.apply({ context, liquid: this.liquid }, [value, ...argv])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { Value } from './value'
|
||||
import { FilterMap } from './filter/filter-map'
|
||||
import { stringify, toValue } from '../util/underscore'
|
||||
import { TemplateImpl } from '../template/template-impl'
|
||||
import { Template } from '../template/template'
|
||||
import { Context } from '../context/context'
|
||||
import { Emitter } from '../render/emitter'
|
||||
import { OutputToken } from '../tokens/output-token'
|
||||
import { Liquid } from '../liquid'
|
||||
|
||||
export class Output extends TemplateImpl<OutputToken> implements Template {
|
||||
private value: Value
|
||||
public constructor (token: OutputToken, filters: FilterMap) {
|
||||
public constructor (token: OutputToken, filters: FilterMap, liquid: Liquid) {
|
||||
super(token)
|
||||
this.value = new Value(token.content, filters)
|
||||
this.value = new Value(token.content, filters, liquid)
|
||||
}
|
||||
public * render (ctx: Context, emitter: Emitter) {
|
||||
const val = yield this.value.value(ctx)
|
||||
emitter.write(stringify(toValue(val)))
|
||||
emitter.write(val)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ export class Hash {
|
||||
* render (ctx: Context) {
|
||||
const hash = {}
|
||||
for (const key of Object.keys(this.hash)) {
|
||||
hash[key] = evalToken(this.hash[key], ctx)
|
||||
hash[key] = yield evalToken(this.hash[key], ctx)
|
||||
}
|
||||
return hash
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { isFunction } from '../../util/underscore'
|
||||
import { Liquid } from '../../liquid'
|
||||
import { TemplateImpl } from '../../template/template-impl'
|
||||
import { Emitter, Hash, Context, TagImplOptions, TagToken, Template, TopLevelToken } from '../../types'
|
||||
import { Emitter, Hash, Context, TagToken, Template, TopLevelToken } from '../../types'
|
||||
import { TagImpl } from './tag-impl'
|
||||
|
||||
export class Tag extends TemplateImpl<TagToken> implements Template {
|
||||
public name: string
|
||||
private impl: TagImpl
|
||||
private static impls: { [key: string]: TagImplOptions } = {}
|
||||
|
||||
public constructor (token: TagToken, tokens: TopLevelToken[], liquid: Liquid) {
|
||||
super(token)
|
||||
|
||||
@@ -4,6 +4,7 @@ import { FilterMap } from '../template/filter/filter-map'
|
||||
import { Filter } from './filter/filter'
|
||||
import { Context } from '../context/context'
|
||||
import { ValueToken } from '../tokens/value-token'
|
||||
import { Liquid } from '../liquid'
|
||||
|
||||
export class Value {
|
||||
public readonly filters: Filter[] = []
|
||||
@@ -12,13 +13,15 @@ export class Value {
|
||||
/**
|
||||
* @param str the value to be valuated, eg.: "foobar" | truncate: 3
|
||||
*/
|
||||
public constructor (str: string, private readonly filterMap: FilterMap) {
|
||||
public constructor (str: string, private readonly filterMap: FilterMap, liquid: Liquid) {
|
||||
const tokenizer = new Tokenizer(str)
|
||||
this.initial = tokenizer.readValue()
|
||||
this.filters = tokenizer.readFilters().map(({ name, args }) => new Filter(name, this.filterMap.get(name), args))
|
||||
this.filters = tokenizer.readFilters().map(({ name, args }) => new Filter(name, this.filterMap.get(name), args, liquid))
|
||||
}
|
||||
public * value (ctx: Context) {
|
||||
let val = yield evalToken(this.initial, ctx)
|
||||
const lenient = ctx.opts.lenientIf && this.filters.length > 0 && this.filters[0].name === 'default'
|
||||
|
||||
let val = yield evalToken(this.initial, ctx, lenient)
|
||||
for (const filter of this.filters) {
|
||||
val = yield filter.render(val, ctx)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Token } from './token'
|
||||
import { ValueToken } from './value-token'
|
||||
import { WordToken } from './word-token'
|
||||
import { IdentifierToken } from './identifier-token'
|
||||
import { TokenKind } from '../parser/token-kind'
|
||||
|
||||
export class HashToken extends Token {
|
||||
@@ -8,7 +8,7 @@ export class HashToken extends Token {
|
||||
public input: string,
|
||||
public begin: number,
|
||||
public end: number,
|
||||
public name: WordToken,
|
||||
public name: IdentifierToken,
|
||||
public value?: ValueToken,
|
||||
public file?: string
|
||||
) {
|
||||
|
||||
@@ -2,8 +2,7 @@ import { Token } from './token'
|
||||
import { NUMBER, TYPES, SIGN } from '../util/character'
|
||||
import { TokenKind } from '../parser/token-kind'
|
||||
|
||||
// a word can be an identifier, a number, a keyword or a single-word-literal
|
||||
export class WordToken extends Token {
|
||||
export class IdentifierToken extends Token {
|
||||
public content: string
|
||||
constructor (
|
||||
public input: string,
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Token } from './token'
|
||||
import { WordToken } from './word-token'
|
||||
import { IdentifierToken } from './identifier-token'
|
||||
import { TokenKind } from '../parser/token-kind'
|
||||
|
||||
export class NumberToken extends Token {
|
||||
constructor (
|
||||
public whole: WordToken,
|
||||
public decimal?: WordToken
|
||||
public whole: IdentifierToken,
|
||||
public decimal?: IdentifierToken
|
||||
) {
|
||||
super(TokenKind.Number, whole.input, whole.begin, decimal ? decimal.end : whole.end, whole.file)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
import { Token } from './token'
|
||||
import { WordToken } from './word-token'
|
||||
import { IdentifierToken } from './identifier-token'
|
||||
import { QuotedToken } from './quoted-token'
|
||||
import { TokenKind } from '../parser/token-kind'
|
||||
import { parseStringLiteral } from '../parser/parse-string-literal'
|
||||
|
||||
export class PropertyAccessToken extends Token {
|
||||
constructor (
|
||||
public variable: WordToken,
|
||||
public props: (WordToken | QuotedToken | PropertyAccessToken)[],
|
||||
public variable: IdentifierToken | QuotedToken,
|
||||
public props: (IdentifierToken | QuotedToken | PropertyAccessToken)[],
|
||||
end: number
|
||||
) {
|
||||
super(TokenKind.PropertyAccess, variable.input, variable.begin, end, variable.file)
|
||||
}
|
||||
|
||||
getVariableAsText () {
|
||||
if (this.variable instanceof IdentifierToken) {
|
||||
return this.variable.getText()
|
||||
} else {
|
||||
return parseStringLiteral(this.variable.getText())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { DelimitedToken } from './delimited-token'
|
||||
import { BLANK, TYPES, VARIABLE } from '../util/character'
|
||||
import { TokenizationError } from '../util/error'
|
||||
import { NormalizedFullOptions } from '../liquid-options'
|
||||
import { TokenKind } from '../parser/token-kind'
|
||||
import { Tokenizer } from '../parser/tokenizer'
|
||||
|
||||
export class TagToken extends DelimitedToken {
|
||||
public name: string
|
||||
@@ -18,13 +18,11 @@ export class TagToken extends DelimitedToken {
|
||||
const value = input.slice(begin + tagDelimiterLeft.length, end - tagDelimiterRight.length)
|
||||
super(TokenKind.Tag, value, input, begin, end, trimTagLeft, trimTagRight, file)
|
||||
|
||||
let nameEnd = 0
|
||||
while (TYPES[this.content.charCodeAt(nameEnd)] & VARIABLE) nameEnd++
|
||||
this.name = this.content.slice(0, nameEnd)
|
||||
const tokenizer = new Tokenizer(this.content)
|
||||
this.name = tokenizer.readIdentifier().getText()
|
||||
if (!this.name) throw new TokenizationError(`illegal tag syntax`, this)
|
||||
|
||||
let argsBegin = nameEnd
|
||||
while (TYPES[this.content.charCodeAt(argsBegin)] & BLANK) argsBegin++
|
||||
this.args = this.content.slice(argsBegin)
|
||||
tokenizer.skipBlank()
|
||||
this.args = tokenizer.remaining()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,3 +17,4 @@ export { TopLevelToken } from './tokens/toplevel-token'
|
||||
export { Tokenizer } from './parser/tokenizer'
|
||||
export { Hash } from './template/tag/hash'
|
||||
export { evalToken, evalQuotedToken } from './render/expression'
|
||||
export { toPromise, toThenable, toValue } from './util/async'
|
||||
|
||||
+16
-10
@@ -7,7 +7,7 @@ interface Thenable {
|
||||
catch (reject: resolver): Thenable;
|
||||
}
|
||||
|
||||
function mkResolve (value: any) {
|
||||
function createResolvedThenable (value: any): Thenable {
|
||||
const ret = {
|
||||
then: (resolve: resolver) => resolve(value),
|
||||
catch: () => ret
|
||||
@@ -15,7 +15,7 @@ function mkResolve (value: any) {
|
||||
return ret
|
||||
}
|
||||
|
||||
function mkReject (err: Error) {
|
||||
function createRejectedThenable (err: Error): Thenable {
|
||||
const ret = {
|
||||
then: (resolve: resolver, reject?: resolver) => {
|
||||
if (reject) return reject(err)
|
||||
@@ -30,43 +30,49 @@ function isThenable (val: any): val is Thenable {
|
||||
return val && isFunction(val.then)
|
||||
}
|
||||
|
||||
function isCustomIterable (val: any): val is IterableIterator<any> {
|
||||
function isAsyncIterator (val: any): val is IterableIterator<any> {
|
||||
return val && isFunction(val.next) && isFunction(val.throw) && isFunction(val.return)
|
||||
}
|
||||
|
||||
// convert an async iterator to a thenable (Promise compatible)
|
||||
export function toThenable (val: IterableIterator<any> | Thenable | any): Thenable {
|
||||
if (isThenable(val)) return val
|
||||
if (isCustomIterable(val)) return reduce()
|
||||
return mkResolve(val)
|
||||
if (isAsyncIterator(val)) return reduce()
|
||||
return createResolvedThenable(val)
|
||||
|
||||
function reduce (prev?: any): Thenable {
|
||||
let state
|
||||
try {
|
||||
state = (val as IterableIterator<any>).next(prev)
|
||||
} catch (err) {
|
||||
return mkReject(err)
|
||||
return createRejectedThenable(err)
|
||||
}
|
||||
|
||||
if (state.done) return mkResolve(state.value)
|
||||
if (state.done) return createResolvedThenable(state.value)
|
||||
return toThenable(state.value!).then(reduce, err => {
|
||||
let state
|
||||
try {
|
||||
state = (val as IterableIterator<any>).throw!(err)
|
||||
} catch (e) {
|
||||
return mkReject(e)
|
||||
return createRejectedThenable(e)
|
||||
}
|
||||
if (state.done) return mkResolve(state.value)
|
||||
if (state.done) return createResolvedThenable(state.value)
|
||||
return reduce(state.value)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function toPromise (val: IterableIterator<any> | Thenable | any): Promise<any> {
|
||||
return Promise.resolve(toThenable(val))
|
||||
}
|
||||
|
||||
// get the value of async iterator in synchronous manner
|
||||
export function toValue (val: IterableIterator<any> | Thenable | any) {
|
||||
let ret: any
|
||||
toThenable(val)
|
||||
.then((x: any) => {
|
||||
ret = x
|
||||
return mkResolve(ret)
|
||||
return createResolvedThenable(ret)
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
throw err
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// This file is generated by bin/character-gen.js
|
||||
// bitmask character types to boost performance
|
||||
export const TYPES = [0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 4, 4, 4, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 2, 8, 0, 0, 0, 0, 8, 0, 0, 0, 64, 0, 65, 0, 0, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 0, 0, 2, 2, 2, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0]
|
||||
export const VARIABLE = 1
|
||||
export const IDENTIFIER = 1
|
||||
export const OPERATOR = 2
|
||||
export const BLANK = 4
|
||||
export const QUOTE = 8
|
||||
|
||||
@@ -48,6 +48,27 @@ export class RenderError extends LiquidError {
|
||||
}
|
||||
}
|
||||
|
||||
export class UndefinedVariableError extends LiquidError {
|
||||
public constructor (err: Error, token: Token) {
|
||||
super(err, token)
|
||||
this.name = 'UndefinedVariableError'
|
||||
this.message = err.message
|
||||
super.update()
|
||||
}
|
||||
}
|
||||
|
||||
// only used internally; raised where we don't have token information,
|
||||
// so it can't be an UndefinedVariableError.
|
||||
export class InternalUndefinedVariableError extends Error {
|
||||
variableName: string
|
||||
|
||||
public constructor (variableName: string) {
|
||||
super(`undefined variable: ${variableName}`)
|
||||
this.name = 'InternalUndefinedVariableError'
|
||||
this.variableName = variableName
|
||||
}
|
||||
}
|
||||
|
||||
export class AssertionError extends Error {
|
||||
public constructor (message: string) {
|
||||
super(message)
|
||||
|
||||
+28
-1
@@ -140,7 +140,12 @@ const formatCodes = {
|
||||
};
|
||||
(formatCodes as any).h = formatCodes.b
|
||||
|
||||
export default function (d: Date, formatStr: string) {
|
||||
export default function (inputDate: Date, formatStr: string) {
|
||||
let d = inputDate
|
||||
if (d instanceof TimezoneDate) {
|
||||
d = d.getDisplayDate()
|
||||
}
|
||||
|
||||
let output = ''
|
||||
let remaining = formatStr
|
||||
let match
|
||||
@@ -168,3 +173,25 @@ function format (d: Date, match: RegExpExecArray) {
|
||||
if (flags['-']) padWidth = 0
|
||||
return padStart(ret, padWidth, padChar)
|
||||
}
|
||||
|
||||
export class TimezoneDate extends Date {
|
||||
ISO8601_TIMEZONE_PATTERN = /([zZ]|([+-])(\d{2}):(\d{2}))$/;
|
||||
|
||||
inputTimezoneOffset = 0;
|
||||
|
||||
constructor (public dateString: string) {
|
||||
super(dateString)
|
||||
const m = dateString.match(this.ISO8601_TIMEZONE_PATTERN)
|
||||
if (m && m[1] === 'Z') {
|
||||
this.inputTimezoneOffset = this.getTimezoneOffset()
|
||||
} else if (m && m[2] && m[3] && m[4]) {
|
||||
const [, , sign, hours, minutes] = m
|
||||
const delta = (sign === '+' ? 1 : -1) * (parseInt(hours, 10) * 60 + parseInt(minutes, 10))
|
||||
this.inputTimezoneOffset = this.getTimezoneOffset() + delta
|
||||
}
|
||||
}
|
||||
|
||||
getDisplayDate (): Date {
|
||||
return new Date((+this) + this.inputTimezoneOffset * 60 * 1000)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { OperatorToken } from '../tokens/operator-token'
|
||||
import { DelimitedToken } from '../tokens/delimited-token'
|
||||
import { WordToken } from '../tokens/word-token'
|
||||
import { IdentifierToken } from '../tokens/identifier-token'
|
||||
import { TagToken } from '../tokens/tag-token'
|
||||
import { HTMLToken } from '../tokens/html-token'
|
||||
import { OutputToken } from '../tokens/output-token'
|
||||
@@ -47,7 +47,7 @@ export function isPropertyAccessToken (val: any): val is PropertyAccessToken {
|
||||
return getKind(val) === TokenKind.PropertyAccess
|
||||
}
|
||||
|
||||
export function isWordToken (val: any): val is WordToken {
|
||||
export function isWordToken (val: any): val is IdentifierToken {
|
||||
return getKind(val) === TokenKind.Word
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ describe('#evalValueSync()', function () {
|
||||
var engine: Liquid
|
||||
beforeEach(() => { engine = new Liquid() })
|
||||
|
||||
it('should throw when scope undefined', async function () {
|
||||
return expect(() => engine.evalValueSync('foo', null as any)).to.throw(/context not defined/)
|
||||
it('should eval value syncly', async function () {
|
||||
return expect(engine.evalValueSync('true', { opts: {} } as any)).to.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,7 +5,8 @@ describe('#evalValue()', function () {
|
||||
var engine: Liquid
|
||||
beforeEach(() => { engine = new Liquid() })
|
||||
|
||||
it('should throw when scope undefined', async function () {
|
||||
return expect(engine.evalValue('"foo"', null as any)).to.be.rejectedWith(/context not defined/)
|
||||
it('should eval value', async function () {
|
||||
const val = await engine.evalValue('true', { opts: {} } as any)
|
||||
expect(val).to.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,4 +10,59 @@ describe('Issues', function () {
|
||||
const html = engine.parseAndRenderSync('{{huh | truncate: 11}}', { huh: 'fdsafdsafdsafdsaaaaa' })
|
||||
expect(html).to.equal('fdsafdsa...')
|
||||
})
|
||||
it('#252 "Not valid identifier" error for a quotes-containing identifier', async () => {
|
||||
const template = `{% capture "form_classes" -%}
|
||||
foo
|
||||
{%- endcapture %}{{form_classes}}`
|
||||
const engine = new Liquid()
|
||||
const html = await engine.parseAndRender(template)
|
||||
expect(html).to.equal('foo')
|
||||
})
|
||||
it('#259 complex property access with braces is not supported', async () => {
|
||||
const engine = new Liquid()
|
||||
const html = engine.parseAndRenderSync('{{ ["complex key"] }}', { 'complex key': 'foo' })
|
||||
expect(html).to.equal('foo')
|
||||
})
|
||||
it('#243 Potential for ReDoS through string replace function', async () => {
|
||||
const engine = new Liquid()
|
||||
const INPUT = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!'
|
||||
const BROKEN_REGEX = /([a-z]+)+$/
|
||||
|
||||
// string filters vulnerable to regexp parameter: split, replace, replace_first, remove_first
|
||||
const parameters = { input: INPUT, regex: BROKEN_REGEX }
|
||||
const template = `{{ input | replace:regex,'' }}`
|
||||
const html = engine.parseAndRenderSync(template, parameters)
|
||||
|
||||
// should stringify the regexp rather than execute it
|
||||
expect(html).to.equal(INPUT)
|
||||
})
|
||||
it('#263 raw/endraw block not ignoring {% characters', () => {
|
||||
const template = `{% raw %}This is a code snippet showing how {% breaks the raw block.{% endraw %}`
|
||||
const engine = new Liquid()
|
||||
const html = engine.parseAndRenderSync(template)
|
||||
expect(html).to.equal('This is a code snippet showing how {% breaks the raw block.')
|
||||
})
|
||||
it('#268 elsif is not supported for unless', () => {
|
||||
const template = `{%- unless condition1 -%}
|
||||
<div>X</div>
|
||||
{%- elsif condition2 -%}
|
||||
<div>Y</div>
|
||||
{%- else %}
|
||||
<div>Z</div>
|
||||
{% endunless %}`
|
||||
const engine = new Liquid()
|
||||
const html = engine.parseAndRenderSync(template, { condition1: true, condition2: true })
|
||||
expect(html).to.equal('<div>Y</div>')
|
||||
})
|
||||
it('#277 Passing liquid in FilterImpl', () => {
|
||||
const engine = new Liquid()
|
||||
engine.registerFilter('render', function (template: string, name: string) {
|
||||
return this.liquid.parseAndRenderSync(decodeURIComponent(template), { name })
|
||||
})
|
||||
const html = engine.parseAndRenderSync(
|
||||
`{{ subtemplate | render: "foo" }}`,
|
||||
{ subtemplate: encodeURIComponent('hello {{ name }}') }
|
||||
)
|
||||
expect(html).to.equal('hello foo')
|
||||
})
|
||||
})
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { Liquid } from '../../dist/liquid.js'
|
||||
import { Liquid } from '../../dist/liquid.browser.umd.js'
|
||||
import * as sinon from 'sinon'
|
||||
import { expect, use } from 'chai'
|
||||
import { JSDOM } from 'jsdom'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { LiquidOptions } from '../../../../src/liquid-options'
|
||||
import { test } from '../../../stub/render'
|
||||
|
||||
describe('filters/date', function () {
|
||||
@@ -11,8 +12,24 @@ describe('filters/date', function () {
|
||||
it('should create a new Date when given "today"', function () {
|
||||
return test('{{ "today" | date: "%Y"}}', (new Date()).getFullYear().toString())
|
||||
})
|
||||
it('should parse as Date when given UTC string', function () {
|
||||
return test('{{ "1991-02-22T00:00:00" | date: "%Y"}}', '1991')
|
||||
it('should parse as Date when given a timezoneless string', function () {
|
||||
return test('{{ "1991-02-22T00:00:00" | date: "%Y-%m-%dT%H:%M:%S"}}', '1991-02-22T00:00:00')
|
||||
})
|
||||
describe('when preserveTimezones is enabled', function () {
|
||||
const opts: LiquidOptions = { preserveTimezones: true }
|
||||
|
||||
it('should not change the timezone between input and output', function () {
|
||||
return test('{{ "1990-12-31T23:00:00Z" | date: "%Y-%m-%dT%H:%M:%S"}}', '1990-12-31T23:00:00', undefined, opts)
|
||||
})
|
||||
it('should apply numeric timezone offset (0)', function () {
|
||||
return test('{{ "1990-12-31T23:00:00+00:00" | date: "%Y-%m-%dT%H:%M:%S"}}', '1990-12-31T23:00:00', undefined, opts)
|
||||
})
|
||||
it('should apply numeric timezone offset (-1)', function () {
|
||||
return test('{{ "1990-12-31T23:00:00-01:00" | date: "%Y-%m-%dT%H:%M:%S"}}', '1990-12-31T23:00:00', undefined, opts)
|
||||
})
|
||||
it('should apply numeric timezone offset (+2.30)', function () {
|
||||
return test('{{ "1990-12-31T23:00:00+02:30" | date: "%Y-%m-%dT%H:%M:%S"}}', '1990-12-31T23:00:00', undefined, opts)
|
||||
})
|
||||
})
|
||||
it('should render string as string if not valid', function () {
|
||||
return test('{{ "foo" | date: "%Y"}}', 'foo')
|
||||
|
||||
@@ -13,6 +13,12 @@ describe('tags/capture', function () {
|
||||
return expect(html).to.equal('A')
|
||||
})
|
||||
|
||||
it('should support quoted variable name', async function () {
|
||||
const src = '{% capture "f" %}{{"a" | capitalize}}{%endcapture%}{{f}}'
|
||||
const html = await liquid.parseAndRender(src)
|
||||
return expect(html).to.equal('A')
|
||||
})
|
||||
|
||||
it('should shading rather than overwriting', async function () {
|
||||
const src = '{% capture var %}10{% endcapture %}{{var}}'
|
||||
const ctx = { 'var': 20 }
|
||||
|
||||
@@ -20,15 +20,23 @@ describe('tags/for', function () {
|
||||
nullProtoObj: Object.create(null),
|
||||
obj: { foo: 'bar', coo: 'haa' },
|
||||
alpha: ['a', 'b', 'c'],
|
||||
promiseArray: Promise.resolve(['a', 'b', 'c']),
|
||||
emptyArray: []
|
||||
}
|
||||
})
|
||||
|
||||
it('should support array', async function () {
|
||||
const src = '{%for c in alpha%}{{c}}{%endfor%}'
|
||||
const html = await liquid.parseAndRender(src, scope)
|
||||
return expect(html).to.equal('abc')
|
||||
})
|
||||
|
||||
it('should support promise of array', async function () {
|
||||
const src = '{%for c in promiseArray%}{{c}}{%endfor%}'
|
||||
const html = await liquid.parseAndRender(src, scope)
|
||||
return expect(html).to.equal('abc')
|
||||
})
|
||||
|
||||
it('should support object', async function () {
|
||||
const src = '{%for item in obj%}{{item[0]}},{{item[1]}}-{%else%}b{%endfor%}'
|
||||
const html = await liquid.parseAndRender(src, scope)
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { Liquid } from '../../../../src/liquid'
|
||||
import { expect } from 'chai'
|
||||
import * as chai from 'chai'
|
||||
import * as chaiAsPromised from 'chai-as-promised'
|
||||
|
||||
const expect = chai.expect
|
||||
chai.use(chaiAsPromised)
|
||||
|
||||
describe('tags/if', function () {
|
||||
const liquid = new Liquid()
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { Liquid } from '../../../../src/liquid'
|
||||
import { expect } from 'chai'
|
||||
import * as chai from 'chai'
|
||||
import * as chaiAsPromised from 'chai-as-promised'
|
||||
|
||||
const expect = chai.expect
|
||||
chai.use(chaiAsPromised)
|
||||
|
||||
describe('tags/raw', function () {
|
||||
const liquid = new Liquid()
|
||||
it('should throw when not closed', async function () {
|
||||
const p = liquid.parseAndRender('{% raw%}')
|
||||
return expect(p).be.rejectedWith(/{% raw%} not closed/)
|
||||
const p = liquid.parseAndRender('{% raw %}')
|
||||
return expect(p).be.rejectedWith(/{% raw %} not closed/)
|
||||
})
|
||||
it('should output filters as it is', async function () {
|
||||
const src = '{% raw %}{{ 5 | plus: 6 }}{% endraw %} is equal to 11.'
|
||||
|
||||
@@ -14,6 +14,16 @@ describe('tags/tablerow', function () {
|
||||
return expect(html).to.equal(dst)
|
||||
})
|
||||
|
||||
it('should support promises', async function () {
|
||||
const src = '{% tablerow i in promiseNumbers %}{{ i }}{% endtablerow %}'
|
||||
const ctx = {
|
||||
promiseNumbers: Promise.resolve([1, 2, 3])
|
||||
}
|
||||
const dst = '<tr class="row1"><td class="col1">1</td><td class="col2">2</td><td class="col3">3</td></tr>'
|
||||
const html = await liquid.parseAndRender(src, ctx)
|
||||
return expect(html).to.equal(dst)
|
||||
})
|
||||
|
||||
it('should support cols', async function () {
|
||||
const src = '{% tablerow i in alpha cols:2 %}{{ i }}{% endtablerow %}'
|
||||
const ctx = {
|
||||
|
||||
@@ -14,6 +14,11 @@ describe('tags/unless', function () {
|
||||
const html = await liquid.parseAndRender(src)
|
||||
return expect(html).to.equal('no')
|
||||
})
|
||||
it('should support elsif', async function () {
|
||||
const src = '{% unless true %}1{%elsif true%}2{%else%}3{%endunless%}'
|
||||
const html = await liquid.parseAndRender(src)
|
||||
return expect(html).to.equal('2')
|
||||
})
|
||||
it('should render unless when predicate yields false', async function () {
|
||||
const src = '{% unless false %}yes{%else%}no{%endunless%}'
|
||||
const html = await liquid.parseAndRender(src)
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { expect } from 'chai'
|
||||
import { Liquid } from '../../../src/liquid'
|
||||
|
||||
describe('LiquidOptions#*keepOutputType*', function () {
|
||||
it('should respect keepOutputType', async function () {
|
||||
const engine = new Liquid({
|
||||
keepOutputType: true
|
||||
})
|
||||
const context = {
|
||||
'my-boolean': true,
|
||||
'my-number': 42,
|
||||
'my-string': 'test'
|
||||
}
|
||||
const booleanHtml = await engine.parseAndRender('{{my-boolean}}', context)
|
||||
expect(booleanHtml).to.equal(true)
|
||||
const numberHtml = await engine.parseAndRender('{{my-number}}', context)
|
||||
expect(numberHtml).to.equal(42)
|
||||
const html = await engine.parseAndRender('{{my-string}}', context)
|
||||
expect(html).to.equal('test')
|
||||
const composedHtml = await engine.parseAndRender('{{my-string}}:{{my-number}}', context)
|
||||
expect(composedHtml).to.equal('test:42')
|
||||
})
|
||||
|
||||
it('should respect keepOutputType = false as default', async function () {
|
||||
const engine = new Liquid()
|
||||
const context = {
|
||||
'my-boolean': true,
|
||||
'my-number': 42,
|
||||
'my-string': 'test'
|
||||
}
|
||||
const booleanHtml = await engine.parseAndRender('{{my-boolean}}', context)
|
||||
expect(booleanHtml).to.equal('true')
|
||||
const numberHtml = await engine.parseAndRender('{{my-number}}', context)
|
||||
expect(numberHtml).to.equal('42')
|
||||
const html = await engine.parseAndRender('{{my-string}}', context)
|
||||
expect(html).to.equal('test')
|
||||
const composedHtml = await engine.parseAndRender('{{my-string}}:{{my-number}}', context)
|
||||
expect(composedHtml).to.equal('test:42')
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Liquid, isFalsy } from '../../../src/liquid'
|
||||
import { Liquid, Context, isFalsy } from '../../../src/liquid'
|
||||
import * as chai from 'chai'
|
||||
import { mock, restore } from '../../stub/mockfs'
|
||||
import * as chaiAsPromised from 'chai-as-promised'
|
||||
@@ -19,7 +19,7 @@ describe('Liquid', function () {
|
||||
it('should call plugin with Liquid', async function () {
|
||||
const engine = new Liquid()
|
||||
engine.plugin(function () {
|
||||
this.registerFilter('t', isFalsy)
|
||||
this.registerFilter('t', function (v) { return isFalsy(v, this.context) })
|
||||
})
|
||||
const html = await engine.parseAndRender('{{false|t}}')
|
||||
expect(html).to.equal('true')
|
||||
@@ -99,14 +99,16 @@ describe('Liquid', function () {
|
||||
describe('#evalValue', function () {
|
||||
it('should eval string literal', async function () {
|
||||
const engine = new Liquid()
|
||||
const str = await engine.evalValue('"foo"', {} as any)
|
||||
const ctx = new Context()
|
||||
const str = await engine.evalValue('"foo"', ctx)
|
||||
expect(str).to.equal('foo')
|
||||
})
|
||||
})
|
||||
describe('#evalValueSync', function () {
|
||||
it('should eval string literal', function () {
|
||||
const engine = new Liquid()
|
||||
const str = engine.evalValueSync('"foo"', {} as any)
|
||||
const ctx = new Context()
|
||||
const str = engine.evalValueSync('"foo"', ctx)
|
||||
expect(str).to.equal('foo')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -30,4 +30,35 @@ describe('LiquidOptions#strict*', function () {
|
||||
return expect(engine.parseAndRender(html, ctx, opts)).to
|
||||
.be.rejectedWith(/undefined variable: notdefined/)
|
||||
})
|
||||
describe('with strictVariables and lenientIf', function () {
|
||||
const strictLenientOpts = {
|
||||
strictVariables: true,
|
||||
lenientIf: true
|
||||
}
|
||||
it('should not throw in `if` with a single variable', async function () {
|
||||
const tpl = engine.parse('before{% if notdefined %}{{notdefined}}{% endif %}after')
|
||||
const html = await engine.render(tpl, ctx, strictLenientOpts)
|
||||
return expect(html).to.equal('beforeafter')
|
||||
})
|
||||
it('should support elsif with undefined variables', async function () {
|
||||
const tpl = engine.parse('{% if notdefined1 %}a{% elsif notdefined2 %}b{% elsif defined3 %}{{defined3}}{% else %}d{% endif %}')
|
||||
const html = await engine.render(tpl, { 'defined3': 'bla' }, strictLenientOpts)
|
||||
return expect(html).to.equal('bla')
|
||||
})
|
||||
it('should not throw in `unless` with a single variable', async function () {
|
||||
const tpl = engine.parse('before{% unless notdefined %}X{% else %}{{notdefined}}{% endunless %}after')
|
||||
const html = await engine.render(tpl, ctx, strictLenientOpts)
|
||||
return expect(html).to.equal('beforeXafter')
|
||||
})
|
||||
it('should still throw with an undefined variable in a compound `if` expression', function () {
|
||||
const tpl = engine.parse('{% if notdefined == 15 %}a{% endif %}')
|
||||
const fhtml = engine.render(tpl, ctx, strictLenientOpts)
|
||||
return expect(fhtml).to.be.rejectedWith(/undefined variable: notdefined/)
|
||||
})
|
||||
it('should allow an undefined variable when before the `default` filter', async function () {
|
||||
const tpl = engine.parse('{{notdefined | default: "a" | tolower}}')
|
||||
const html = await engine.render(tpl, ctx, strictLenientOpts)
|
||||
return expect(html).to.equal('a')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
+5
-4
@@ -1,16 +1,17 @@
|
||||
import { Liquid } from '../../src/liquid'
|
||||
import { LiquidOptions } from '../../src/liquid-options'
|
||||
import { expect } from 'chai'
|
||||
|
||||
export const liquid = new Liquid()
|
||||
|
||||
export function render (src: string, ctx?: object) {
|
||||
return liquid.parseAndRender(src, ctx)
|
||||
export function render (src: string, ctx?: object, opts?: LiquidOptions) {
|
||||
return liquid.parseAndRender(src, ctx, opts)
|
||||
}
|
||||
|
||||
export async function test (src: string, ctx: object | string, dst?: string) {
|
||||
export async function test (src: string, ctx: object | string, dst?: string, opts?: LiquidOptions) {
|
||||
if (dst === undefined) {
|
||||
dst = ctx as string
|
||||
ctx = {}
|
||||
}
|
||||
return expect(await render(src, ctx as object)).to.equal(dst)
|
||||
return expect(await render(src, ctx as object, opts)).to.equal(dst)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect } from 'chai'
|
||||
import { WordToken } from '../../../src/tokens/word-token'
|
||||
import { IdentifierToken } from '../../../src/tokens/identifier-token'
|
||||
import { NumberToken } from '../../../src/tokens/number-token'
|
||||
import { PropertyAccessToken } from '../../../src/tokens/property-access-token'
|
||||
import { RangeToken } from '../../../src/tokens/range-token'
|
||||
@@ -19,6 +19,10 @@ describe('Tokenize', function () {
|
||||
expect(new Tokenizer('a[ b][ "c d" ]').readValueOrThrow().getText()).to.equal('a[ b][ "c d" ]')
|
||||
expect(new Tokenizer('a.b[c[d.e]]').readValueOrThrow().getText()).to.equal('a.b[c[d.e]]')
|
||||
})
|
||||
it('should read identifier', () => {
|
||||
expect(new Tokenizer('foo bar').readIdentifier()).to.haveOwnProperty('content', 'foo')
|
||||
expect(new Tokenizer('foo bar').readWord()).to.haveOwnProperty('content', 'foo')
|
||||
})
|
||||
it('should read number value', () => {
|
||||
const token: NumberToken = new Tokenizer('2.33.2').readValueOrThrow() as any
|
||||
expect(token).to.be.instanceOf(NumberToken)
|
||||
@@ -34,6 +38,19 @@ describe('Tokenize', function () {
|
||||
it('should read property access value', () => {
|
||||
expect(new Tokenizer('a[b]["c d"]').readValueOrThrow().getText()).to.equal('a[b]["c d"]')
|
||||
})
|
||||
it('should read quoted property access value', () => {
|
||||
const value = new Tokenizer('["a prop"]').readValue()
|
||||
expect(value).to.be.instanceOf(PropertyAccessToken)
|
||||
expect((value as PropertyAccessToken).variable.getText()).to.equal('"a prop"')
|
||||
})
|
||||
it('should throw for broken quoted property access', () => {
|
||||
const tokenizer = new Tokenizer('[5]')
|
||||
expect(() => tokenizer.readValueOrThrow()).to.throw()
|
||||
})
|
||||
it('should throw for incomplete quoted property access', () => {
|
||||
const tokenizer = new Tokenizer('["a prop"')
|
||||
expect(() => tokenizer.readValueOrThrow()).to.throw()
|
||||
})
|
||||
it('should read hash', () => {
|
||||
const hash1 = new Tokenizer('foo: 3').readHash()
|
||||
expect(hash1!.name.content).to.equal('foo')
|
||||
@@ -87,6 +104,29 @@ describe('Tokenize', function () {
|
||||
expect(tag.name).to.equal('for')
|
||||
expect(tag.args).to.equal('p in a[1]')
|
||||
})
|
||||
it('should allow unclosed tag inside {% raw %}', function () {
|
||||
const html = '{%raw%} {%if%} {%else {%endraw%}'
|
||||
const tokenizer = new Tokenizer(html)
|
||||
const tokens = tokenizer.readTopLevelTokens()
|
||||
|
||||
expect(tokens.length).to.equal(3)
|
||||
expect(tokens[0]).to.haveOwnProperty('name', 'raw')
|
||||
expect((tokens[1] as any).getContent()).to.equal(' {%if%} {%else ')
|
||||
})
|
||||
it('should allow unclosed endraw tag inside {% raw %}', function () {
|
||||
const html = '{%raw%} {%endraw {%raw%} {%endraw%}'
|
||||
const tokenizer = new Tokenizer(html)
|
||||
const tokens = tokenizer.readTopLevelTokens()
|
||||
|
||||
expect(tokens.length).to.equal(3)
|
||||
expect(tokens[0]).to.haveOwnProperty('name', 'raw')
|
||||
expect((tokens[1] as any).getContent()).to.equal(' {%endraw {%raw%} ')
|
||||
})
|
||||
it('should throw when {% raw %} not closed', function () {
|
||||
const html = '{%raw%} {%endraw {%raw%}'
|
||||
const tokenizer = new Tokenizer(html)
|
||||
expect(() => tokenizer.readTopLevelTokens()).to.throw('raw "{%raw%} {%end..." not closed, line:1, col:8')
|
||||
})
|
||||
it('should read output token', function () {
|
||||
const html = '<p>{{foo | date: "%Y-%m-%d"}}</p>'
|
||||
const tokenizer = new Tokenizer(html)
|
||||
@@ -246,7 +286,7 @@ describe('Tokenize', function () {
|
||||
expect(token!.args[0]).to.be.instanceOf(PropertyAccessToken)
|
||||
expect(pa.variable.content).to.equal('obj')
|
||||
expect(pa.props).to.have.lengthOf(1)
|
||||
expect(pa.props[0]).to.be.instanceOf(WordToken)
|
||||
expect(pa.props[0]).to.be.instanceOf(IdentifierToken)
|
||||
expect(pa.props[0].getText()).to.equal('foo')
|
||||
})
|
||||
it('should read a filter with obj["foo"] argument', function () {
|
||||
@@ -316,7 +356,7 @@ describe('Tokenize', function () {
|
||||
expect(pa.props).to.have.lengthOf(2)
|
||||
|
||||
const [p1, p2] = pa.props
|
||||
expect(p1).to.be.instanceOf(WordToken)
|
||||
expect(p1).to.be.instanceOf(IdentifierToken)
|
||||
expect(p1.getText()).to.equal('')
|
||||
expect(p2).to.be.instanceOf(PropertyAccessToken)
|
||||
expect(p2.getText()).to.equal('b')
|
||||
|
||||
+69
-12
@@ -1,38 +1,95 @@
|
||||
import { isTruthy } from '../../../src/render/boolean'
|
||||
import { isTruthy, isFalsy } from '../../../src/render/boolean'
|
||||
import { Context } from '../../../src/context/context'
|
||||
import { expect } from 'chai'
|
||||
|
||||
describe('boolean', async function () {
|
||||
describe('boolean Shopify', async function () {
|
||||
describe('.isTruthy()', async function () {
|
||||
const ctx = {
|
||||
opts: {
|
||||
jsTruthy: false
|
||||
}
|
||||
} as unknown as Context
|
||||
//
|
||||
// Spec: https://shopify.github.io/liquid/basics/truthy-and-falsy/
|
||||
it('true is truthy', function () {
|
||||
expect(isTruthy(true)).to.be.true
|
||||
expect(isTruthy(true, ctx)).to.be.true
|
||||
})
|
||||
it('false is falsy', function () {
|
||||
expect(isTruthy(false)).to.be.false
|
||||
expect(isTruthy(false, ctx)).to.be.false
|
||||
})
|
||||
it('null is falsy', function () {
|
||||
expect(isTruthy(null)).to.be.false
|
||||
expect(isTruthy(null, ctx)).to.be.false
|
||||
})
|
||||
it('"foo" is truthy', function () {
|
||||
expect(isTruthy('foo')).to.be.true
|
||||
expect(isTruthy('foo', ctx)).to.be.true
|
||||
})
|
||||
it('"" is truthy', function () {
|
||||
expect(isTruthy('')).to.be.true
|
||||
expect(isTruthy('', ctx)).to.be.true
|
||||
})
|
||||
it('0 is truthy', function () {
|
||||
expect(isTruthy(0)).to.be.true
|
||||
expect(isTruthy(0, ctx)).to.be.true
|
||||
})
|
||||
it('1 is truthy', function () {
|
||||
expect(isTruthy(1)).to.be.true
|
||||
expect(isTruthy(1, ctx)).to.be.true
|
||||
})
|
||||
it('1.1 is truthy', function () {
|
||||
expect(isTruthy(1.1)).to.be.true
|
||||
expect(isTruthy(1.1, ctx)).to.be.true
|
||||
})
|
||||
it('[1] is truthy', function () {
|
||||
expect(isTruthy([1])).to.be.true
|
||||
expect(isTruthy([1], ctx)).to.be.true
|
||||
})
|
||||
it('[] is truthy', function () {
|
||||
expect(isTruthy([])).to.be.true
|
||||
expect(isTruthy([], ctx)).to.be.true
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('boolean jsTruthy', async function () {
|
||||
const ctx = {
|
||||
opts: {
|
||||
jsTruthy: true
|
||||
}
|
||||
} as unknown as Context
|
||||
|
||||
describe('.isFalsy()', async function () {
|
||||
it('null is always falsy', function () {
|
||||
expect(isFalsy(null, ctx)).to.be.true
|
||||
})
|
||||
})
|
||||
|
||||
describe('.isTruthy()', async function () {
|
||||
it('true is truthy', function () {
|
||||
expect(isTruthy(true, ctx)).to.be.true
|
||||
})
|
||||
it('false is falsy', function () {
|
||||
expect(isTruthy(false, ctx)).to.be.false
|
||||
})
|
||||
it('null is always falsy', function () {
|
||||
expect(isTruthy(null, ctx)).to.be.false
|
||||
})
|
||||
it('null is always falsy', function () {
|
||||
expect(isTruthy(null, ctx)).to.be.false
|
||||
})
|
||||
it('"foo" is truthy', function () {
|
||||
expect(isTruthy('foo', ctx)).to.be.true
|
||||
})
|
||||
it('"" is falsy', function () {
|
||||
expect(isTruthy('', ctx)).to.be.false
|
||||
})
|
||||
it('0 is falsy', function () {
|
||||
expect(isTruthy(0, ctx)).to.be.false
|
||||
})
|
||||
it('1 is truthy', function () {
|
||||
expect(isTruthy(1, ctx)).to.be.true
|
||||
})
|
||||
it('1.1 is truthy', function () {
|
||||
expect(isTruthy(1.1, ctx)).to.be.true
|
||||
})
|
||||
it('[1] is truthy', function () {
|
||||
expect(isTruthy([1], ctx)).to.be.true
|
||||
})
|
||||
it('[] is truthy', function () {
|
||||
expect(isTruthy([], ctx)).to.be.true
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Context } from '../../../src/context/context'
|
||||
import { HTMLToken } from '../../../src/tokens/html-token'
|
||||
import { Render } from '../../../src/render/render'
|
||||
import { HTML } from '../../../src/template/html'
|
||||
import { Emitter } from '../../../src/render/emitter'
|
||||
import { toThenable } from '../../../src/util/async'
|
||||
|
||||
describe('render', function () {
|
||||
@@ -15,7 +16,7 @@ describe('render', function () {
|
||||
it('should render html', async function () {
|
||||
const scope = new Context()
|
||||
const token = { getContent: () => '<p>' } as HTMLToken
|
||||
const html = await toThenable(render.renderTemplates([new HTML(token)], scope))
|
||||
const html = await toThenable(render.renderTemplates([new HTML(token)], scope, new Emitter(scope.opts.keepOutputType)))
|
||||
return expect(html).to.equal('<p>')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Context } from '../../../../src/context/context'
|
||||
import { toThenable } from '../../../../src/util/async'
|
||||
import { NumberToken } from '../../../../src/tokens/number-token'
|
||||
import { QuotedToken } from '../../../../src/tokens/quoted-token'
|
||||
import { WordToken } from '../../../../src/tokens/word-token'
|
||||
import { IdentifierToken } from '../../../../src/tokens/identifier-token'
|
||||
import { FilterMap } from '../../../../src/template/filter/filter-map'
|
||||
|
||||
chai.use(sinonChai)
|
||||
@@ -14,8 +14,9 @@ const expect = chai.expect
|
||||
describe('filter', function () {
|
||||
let ctx: Context
|
||||
let filters: FilterMap
|
||||
const liquid = {} as any
|
||||
beforeEach(function () {
|
||||
filters = new FilterMap(false)
|
||||
filters = new FilterMap(false, liquid)
|
||||
ctx = new Context()
|
||||
})
|
||||
it('should create default filter if not registered', async function () {
|
||||
@@ -30,16 +31,17 @@ describe('filter', function () {
|
||||
it('should call filter impl with correct arguments', async function () {
|
||||
const spy = sinon.spy()
|
||||
filters.set('foo', spy)
|
||||
const thirty = new NumberToken(new WordToken('30', 0, 2), undefined)
|
||||
const thirty = new NumberToken(new IdentifierToken('30', 0, 2), undefined)
|
||||
await toThenable(filters.create('foo', [thirty]).render('foo', ctx))
|
||||
expect(spy).to.have.been.calledWith('foo', 30)
|
||||
})
|
||||
it('should call filter impl with correct this arg', async function () {
|
||||
it('should call filter impl with correct this', async function () {
|
||||
const spy = sinon.spy()
|
||||
filters.set('foo', spy)
|
||||
const thirty = new NumberToken(new WordToken('33', 0, 2), undefined)
|
||||
const thirty = new NumberToken(new IdentifierToken('33', 0, 2), undefined)
|
||||
await toThenable(filters.create('foo', [thirty]).render('foo', ctx))
|
||||
expect(spy).to.have.been.calledOn(sinon.match.has('context', ctx))
|
||||
expect(spy).to.have.been.calledOn(sinon.match.has('liquid', liquid))
|
||||
})
|
||||
it('should render a simple filter', async function () {
|
||||
filters.set('upcase', x => x.toUpperCase())
|
||||
@@ -48,13 +50,13 @@ describe('filter', function () {
|
||||
|
||||
it('should render filters with argument', async function () {
|
||||
filters.set('add', (a, b) => a + b)
|
||||
const two = new NumberToken(new WordToken('2', 0, 1), undefined)
|
||||
const two = new NumberToken(new IdentifierToken('2', 0, 1), undefined)
|
||||
expect(await toThenable(filters.create('add', [two]).render(3, ctx))).to.equal(5)
|
||||
})
|
||||
|
||||
it('should render filters with multiple arguments', async function () {
|
||||
filters.set('add', (a, b, c) => a + b + c)
|
||||
const two = new NumberToken(new WordToken('2', 0, 1), undefined)
|
||||
const two = new NumberToken(new IdentifierToken('2', 0, 1), undefined)
|
||||
const c = new QuotedToken('"c"', 0, 3)
|
||||
expect(await toThenable(filters.create('add', [two, c]).render(3, ctx))).to.equal('5c')
|
||||
})
|
||||
@@ -73,7 +75,7 @@ describe('filter', function () {
|
||||
|
||||
it('should support key value pairs', async function () {
|
||||
filters.set('add', (a, b) => b[0] + ':' + (a + b[1]))
|
||||
const two = new NumberToken(new WordToken('2', 0, 1), undefined)
|
||||
const two = new NumberToken(new IdentifierToken('2', 0, 1), undefined)
|
||||
expect(await toThenable((filters.create('add', [['num', two]]).render(3, ctx)))).to.equal('num:5')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,14 +4,16 @@ import { Context } from '../../../src/context/context'
|
||||
import { Output } from '../../../src/template/output'
|
||||
import { OutputToken } from '../../../src/tokens/output-token'
|
||||
import { FilterMap } from '../../../src/template/filter/filter-map'
|
||||
import { defaultOptions } from '../../../src/liquid-options'
|
||||
|
||||
const expect = chai.expect
|
||||
|
||||
describe('Output', function () {
|
||||
const emitter: any = { write: (html: string) => (emitter.html += html), html: '' }
|
||||
const liquid = {} as any
|
||||
let filters: FilterMap
|
||||
beforeEach(function () {
|
||||
filters = new FilterMap(false)
|
||||
filters = new FilterMap(false, liquid)
|
||||
emitter.html = ''
|
||||
})
|
||||
|
||||
@@ -19,26 +21,77 @@ describe('Output', function () {
|
||||
const scope = new Context({
|
||||
foo: { obj: { arr: ['a', 2] } }
|
||||
})
|
||||
const output = new Output({ content: 'foo' } as OutputToken, filters)
|
||||
const output = new Output({ content: 'foo' } as OutputToken, filters, liquid)
|
||||
await toThenable(output.render(scope, emitter))
|
||||
return expect(emitter.html).to.equal('[object Object]')
|
||||
})
|
||||
it('should skip function property', async function () {
|
||||
const scope = new Context({ obj: { foo: 'foo', bar: (x: any) => x } })
|
||||
const output = new Output({ content: 'obj' } as OutputToken, filters)
|
||||
const output = new Output({ content: 'obj' } as OutputToken, filters, liquid)
|
||||
await toThenable(output.render(scope, emitter))
|
||||
return expect(emitter.html).to.equal('[object Object]')
|
||||
})
|
||||
it('should respect to .toString()', async () => {
|
||||
const scope = new Context({ obj: { toString: () => 'FOO' } })
|
||||
const output = new Output({ content: 'obj' } as OutputToken, filters)
|
||||
const output = new Output({ content: 'obj' } as OutputToken, filters, liquid)
|
||||
await toThenable(output.render(scope, emitter))
|
||||
return expect(emitter.html).to.equal('FOO')
|
||||
})
|
||||
it('should respect to .toString()', async () => {
|
||||
const scope = new Context({ obj: { toString: () => 'FOO' } })
|
||||
const output = new Output({ content: 'obj' } as OutputToken, filters)
|
||||
const output = new Output({ content: 'obj' } as OutputToken, filters, liquid)
|
||||
await toThenable(output.render(scope, emitter))
|
||||
return expect(emitter.html).to.equal('FOO')
|
||||
})
|
||||
context('when keepOutputType is enabled', () => {
|
||||
const emitter: any = {
|
||||
write: (html: any) => {
|
||||
if (emitter.keepOutputType && typeof html !== 'string') {
|
||||
emitter.html = html
|
||||
} else {
|
||||
emitter.html += html as string
|
||||
}
|
||||
},
|
||||
html: '',
|
||||
keepOutputType: true
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
filters = new FilterMap(false, liquid)
|
||||
emitter.html = ''
|
||||
})
|
||||
|
||||
it('should respect output variable number type', async () => {
|
||||
const scope = new Context({
|
||||
foo: 42
|
||||
}, { ...defaultOptions, keepOutputType: true })
|
||||
const output = new Output({ content: 'foo' } as OutputToken, filters, liquid)
|
||||
await toThenable(output.render(scope, emitter))
|
||||
return expect(emitter.html).to.equal(42)
|
||||
})
|
||||
it('should respect output variable boolean type', async () => {
|
||||
const scope = new Context({
|
||||
foo: true
|
||||
}, { ...defaultOptions, keepOutputType: true })
|
||||
const output = new Output({ content: 'foo' } as OutputToken, filters, liquid)
|
||||
await toThenable(output.render(scope, emitter))
|
||||
return expect(emitter.html).to.equal(true)
|
||||
})
|
||||
it('should respect output variable object type', async () => {
|
||||
const scope = new Context({
|
||||
foo: 'test'
|
||||
}, { ...defaultOptions, keepOutputType: true })
|
||||
const output = new Output({ content: 'foo' } as OutputToken, filters, liquid)
|
||||
await toThenable(output.render(scope, emitter))
|
||||
return expect(emitter.html).to.equal('test')
|
||||
})
|
||||
it('should respect output variable string type', async () => {
|
||||
const scope = new Context({
|
||||
foo: { a: { b: 42 } }
|
||||
}, { ...defaultOptions, keepOutputType: true })
|
||||
const output = new Output({ content: 'foo' } as OutputToken, filters, liquid)
|
||||
await toThenable(output.render(scope, emitter))
|
||||
return expect(emitter.html).to.deep.equal({ a: { b: 42 } })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -12,15 +12,17 @@ chai.use(sinonChai)
|
||||
const expect = chai.expect
|
||||
|
||||
describe('Value', function () {
|
||||
const liquid = {} as any
|
||||
|
||||
describe('#constructor()', function () {
|
||||
const filterMap = new FilterMap(false)
|
||||
const filterMap = new FilterMap(false, liquid)
|
||||
it('should parse "foo', function () {
|
||||
const tpl = new Value('foo', filterMap)
|
||||
const tpl = new Value('foo', filterMap, liquid)
|
||||
expect(tpl.initial!.getText()).to.equal('foo')
|
||||
expect(tpl.filters).to.deep.equal([])
|
||||
})
|
||||
it('should parse filters in value content', function () {
|
||||
const f = new Value('o | foo: a: "a"', filterMap)
|
||||
const f = new Value('o | foo: a: "a"', filterMap, liquid)
|
||||
expect(f.filters[0].name).to.equal('foo')
|
||||
expect(f.filters[0].args).to.have.lengthOf(1)
|
||||
const [k, v] = f.filters[0].args[0] as any
|
||||
@@ -34,10 +36,10 @@ describe('Value', function () {
|
||||
it('should call chained filters correctly', async function () {
|
||||
const date = sinon.stub().returns('y')
|
||||
const time = sinon.spy()
|
||||
const filterMap = new FilterMap(false)
|
||||
const filterMap = new FilterMap(false, liquid)
|
||||
filterMap.set('date', date)
|
||||
filterMap.set('time', time)
|
||||
const tpl = new Value('foo.bar | date: "b" | time:2', filterMap)
|
||||
const tpl = new Value('foo.bar | date: "b" | time:2', filterMap, liquid)
|
||||
const scope = new Context({
|
||||
foo: { bar: 'bar' }
|
||||
})
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import * as chai from 'chai'
|
||||
import * as sinonChai from 'sinon-chai'
|
||||
import { PropertyAccessToken } from '../../../src/tokens/property-access-token'
|
||||
import { QuotedToken } from '../../../src/tokens/quoted-token'
|
||||
import { IdentifierToken } from '../../../src/tokens/identifier-token'
|
||||
|
||||
chai.use(sinonChai)
|
||||
const expect = chai.expect
|
||||
|
||||
describe('PropertyAccessToken', function () {
|
||||
describe('getVariableAsText', function () {
|
||||
it('should return correct value for IdentifierToken', function () {
|
||||
const token = new PropertyAccessToken(new IdentifierToken('foo', 0, 3), [], 3)
|
||||
expect(token.getVariableAsText()).to.equal('foo')
|
||||
})
|
||||
it('should return correct value for QuotedToken', function () {
|
||||
const token = new PropertyAccessToken(new QuotedToken('"foo bar"', 0, 9), [], 9)
|
||||
expect(token.getVariableAsText()).to.equal('foo bar')
|
||||
})
|
||||
})
|
||||
})
|
||||
+10
-1
@@ -1,10 +1,19 @@
|
||||
import { toThenable, toValue } from '../../../src/util/async'
|
||||
import { toThenable, toPromise, toValue } from '../../../src/util/async'
|
||||
import { expect, use } from 'chai'
|
||||
import * as chaiAsPromised from 'chai-as-promised'
|
||||
|
||||
use(chaiAsPromised)
|
||||
|
||||
describe('utils/async', () => {
|
||||
describe('#toPromise()', function () {
|
||||
it('should return a promise', async () => {
|
||||
function * foo () {
|
||||
return 'foo'
|
||||
}
|
||||
const result = await toPromise(foo())
|
||||
expect(result).to.equal('foo')
|
||||
})
|
||||
})
|
||||
describe('#toThenable()', function () {
|
||||
it('should support iterable with single return statement', async () => {
|
||||
function * foo () {
|
||||
|
||||
@@ -25,18 +25,18 @@ describe('util/strftime', function () {
|
||||
expect(t(then, '%j')).to.equal('066')
|
||||
})
|
||||
it('should take count of leap years', function () {
|
||||
const date = new Date('2001-03-01')
|
||||
const date = new Date('2001 03 01')
|
||||
expect(t(date, '%j')).to.equal('060')
|
||||
})
|
||||
it('should take count of leap years', function () {
|
||||
const date = new Date('2000-03-01')
|
||||
const date = new Date('2000 03 01')
|
||||
expect(t(date, '%j')).to.equal('061')
|
||||
})
|
||||
})
|
||||
it('should format %q as date suffix', function () {
|
||||
const st = new Date('2016-03-01T03:05:03.000Z')
|
||||
const nd = new Date('2016-03-02T03:05:03.000Z')
|
||||
const rd = new Date('2016-03-03T03:05:03.000Z')
|
||||
const st = new Date('2016-03-01 03:05:03')
|
||||
const nd = new Date('2016-03-02 03:05:03')
|
||||
const rd = new Date('2016-03-03 03:05:03')
|
||||
expect(t(st, '%q')).to.equal('st')
|
||||
expect(t(nd, '%q')).to.equal('nd')
|
||||
expect(t(rd, '%q')).to.equal('rd')
|
||||
|
||||
Reference in New Issue
Block a user