mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-15 04:10:40 -07:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1956096917 | ||
|
|
66c1cdecbf |
+29
-55
@@ -321,7 +321,8 @@
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/2703309?v=4",
|
||||
"profile": "https://github.com/amit777",
|
||||
"contributions": [
|
||||
"code"
|
||||
"code",
|
||||
"financial"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -586,6 +587,33 @@
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Checkout-Blocks",
|
||||
"name": "Checkout Blocks",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/114603307?v=4",
|
||||
"profile": "https://www.checkoutblocks.com/",
|
||||
"contributions": [
|
||||
"financial"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "akdarrah",
|
||||
"name": "Adam Darrah",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/69064?v=4",
|
||||
"profile": "https://www.dropkiq.com/",
|
||||
"contributions": [
|
||||
"financial"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "11ty",
|
||||
"name": "Eleventy",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/35147177?v=4",
|
||||
"profile": "https://www.11ty.dev/",
|
||||
"contributions": [
|
||||
"financial"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "NReilingh",
|
||||
"name": "Nick Reilingh",
|
||||
@@ -594,60 +622,6 @@
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "ebobby",
|
||||
"name": "Francisco Soto",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/170356?v=4",
|
||||
"profile": "http://ebobby.org",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "davidlj95",
|
||||
"name": "David LJ",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/8050648?v=4",
|
||||
"profile": "https://www.davidlj95.com",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "RasmusWL",
|
||||
"name": "Rasmus Wriedt Larsen",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1054041?v=4",
|
||||
"profile": "https://github.com/RasmusWL",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "brunodccarvalho",
|
||||
"name": "Bruno Carvalho",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/24962950?v=4",
|
||||
"profile": "https://github.com/brunodccarvalho",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "fupengl",
|
||||
"name": "傅鹏",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/20211964?v=4",
|
||||
"profile": "https://github.com/fupengl",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "joel-hamilton",
|
||||
"name": "Joel Hamilton",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/12899024?v=4",
|
||||
"profile": "https://github.com/joel-hamilton",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
name: Release
|
||||
on: workflow_dispatch
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
uses: ./.github/workflows/build.yml
|
||||
with:
|
||||
os: ${{ matrix.os }}
|
||||
test:
|
||||
needs: build
|
||||
uses: ./.github/workflows/test.yml
|
||||
release:
|
||||
name: Release
|
||||
needs: [build, test]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -15,6 +26,11 @@ jobs:
|
||||
node-version: '18'
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: dist-ubuntu-latest
|
||||
path: dist
|
||||
- name: Release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -1,68 +1,3 @@
|
||||
## [10.10.2](https://github.com/harttle/liquidjs/compare/v10.10.1...v10.10.2) (2024-03-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* contains regression ([#677](https://github.com/harttle/liquidjs/issues/677)) ([05223c4](https://github.com/harttle/liquidjs/commit/05223c4378f9474f4e658af36cb8272e161d681f)), closes [#675](https://github.com/harttle/liquidjs/issues/675)
|
||||
|
||||
## [10.10.1](https://github.com/harttle/liquidjs/compare/v10.10.0...v10.10.1) (2024-02-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* in conditionals, don't render anything after an else branch ([#671](https://github.com/harttle/liquidjs/issues/671)) ([f816955](https://github.com/harttle/liquidjs/commit/f81695570491ede77975de2c26a07612a2d62c28))
|
||||
* Rely on equal for computing contains ([#668](https://github.com/harttle/liquidjs/issues/668)) ([1937aa1](https://github.com/harttle/liquidjs/commit/1937aa1f1dce01ee6332f39a6e658e85cbe4f30b))
|
||||
|
||||
# [10.10.0](https://github.com/harttle/liquidjs/compare/v10.9.4...v10.10.0) (2023-12-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Array sum filter ([#661](https://github.com/harttle/liquidjs/issues/661)) ([629d958](https://github.com/harttle/liquidjs/commit/629d958b86a97ddf2921d2285b7c9ea83430004e))
|
||||
|
||||
## [10.9.4](https://github.com/harttle/liquidjs/compare/v10.9.3...v10.9.4) (2023-11-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* allow unicode to be identifiers, fixes [#655](https://github.com/harttle/liquidjs/issues/655) ([dd7616a](https://github.com/harttle/liquidjs/commit/dd7616acb9a71b77f39d2fa24b6f68a7caef87f1))
|
||||
|
||||
## [10.9.3](https://github.com/harttle/liquidjs/compare/v10.9.2...v10.9.3) (2023-10-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* package version in released files ([67a5b22](https://github.com/harttle/liquidjs/commit/67a5b229ca9ccabb4aee42bbb5c9f03d4076786a))
|
||||
|
||||
## [10.9.2](https://github.com/harttle/liquidjs/compare/v10.9.1...v10.9.2) (2023-08-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* handle windows newlines on `newline_to_br` and `strip_newlines` ([88aa63f](https://github.com/harttle/liquidjs/commit/88aa63fd58b5a5824c031acc6f3e4072bedd262f))
|
||||
* sort and where bug when using `strictVariables` ([8af682d](https://github.com/harttle/liquidjs/commit/8af682d2ca68de99bafd4a7055e4912eeb318f57))
|
||||
|
||||
## [10.9.1](https://github.com/harttle/liquidjs/compare/v10.9.0...v10.9.1) (2023-08-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* map filter allow nil results in strict mode, fixes [#647](https://github.com/harttle/liquidjs/issues/647) ([45adbd7](https://github.com/harttle/liquidjs/commit/45adbd7008296a94da04d21a35917f744a0f4109))
|
||||
|
||||
# [10.9.0](https://github.com/harttle/liquidjs/compare/v10.8.4...v10.9.0) (2023-08-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* case should allow multiple values separated by or ([b8e7e2d](https://github.com/harttle/liquidjs/commit/b8e7e2d9467b17ca786e6fb422e9579dd178de76))
|
||||
* for throws undefined var with a null value with strictVariables ([dc6a301](https://github.com/harttle/liquidjs/commit/dc6a3013874872ac85f1fbe5184c74631122d851))
|
||||
* remove_last was eating an extra character ([fc27313](https://github.com/harttle/liquidjs/commit/fc2731376f8ef59ac7160f97cef1fb5d94f053db))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* more flexible squared property read expression, fixes [#643](https://github.com/harttle/liquidjs/issues/643) ([#646](https://github.com/harttle/liquidjs/issues/646)) ([660d9be](https://github.com/harttle/liquidjs/commit/660d9be55f8eac16ca5ac77fd0b38b0d7f94961e))
|
||||
|
||||
## [10.8.4](https://github.com/harttle/liquidjs/compare/v10.8.3...v10.8.4) (2023-07-07)
|
||||
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ A simple, expressive and safe [Shopify][shopify/liquid] / Github Pages compatibl
|
||||
|
||||
* [Documentation][doc]
|
||||
* Please star [LiquidJS on GitHub][github]!
|
||||
* Financial support via [Github Sponsors](https://github.com/sponsors/harttle), or [LiquidJS on Open Collective][oc]
|
||||
* Financial support via [LiquidJS on Open Collective][oc], or [Github Sponsors](https://github.com/sponsors/harttle)
|
||||
|
||||
<p align="center"><a href="https://liquidjs.com"><img height="155px" width="155px" src="https://liquidjs.com/icon/mstile-310x310.png" alt="logo"></a></p>
|
||||
|
||||
@@ -67,34 +67,17 @@ Feel free to create a PR or contact me to add your use case into this list!
|
||||
|
||||
## Financial Support
|
||||
|
||||
If you personally love LiquidJS or it's benefiting your business, please consider financially support us:
|
||||
If you love LiquidJS or your company is using LiquidJS, please consider [financial support][financial-support].
|
||||
|
||||
- via [Github Sponsors](https://github.com/sponsors/harttle), or
|
||||
- via [LiquidJS on Open Collective][oc]
|
||||
### Backers
|
||||
If you personally like LiquidJS and find it useful to you, buy me a coffee!💖
|
||||
|
||||
Special thanks to the following sponsors!
|
||||
<img src="https://opencollective.com/liquidjs/backers.svg?avatarHeight=72">
|
||||
|
||||
<!-- FINANCIAL-CONTRIBUTORS-BEGIN -->
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.opensense.com/"><img src="https://images.opencollective.com/opensense-inc/bf840ae/logo/256.png?height=100" width="100px;" alt="Opensense Inc."/><br /><sub><b>Opensense</b></sub></a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.11ty.dev/"><img src="https://avatars.githubusercontent.com/u/35147177?v=4?s=100" width="100px;" alt="Eleventy"/><br /><sub><b>Eleventy</b></sub></a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://about.me/peterdehaan"><img src="https://avatars2.githubusercontent.com/u/557895?v=4?s=100" width="100px;" alt="Peter deHaan"/><br /><sub><b>Peter deHaan</b></sub></a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://opencollective.com/touchless"><img src="https://images.opencollective.com/touchless/273bc74/logo/256.png?height=100" width="100px;" alt="Touchless"/><br /><sub><b>Touchless</b></sub></a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.dropkiq.com/"><img src="https://images.opencollective.com/1bertlol/43a8ea8/logo/256.png?height=100" width="100px;" alt="Adam Darrah"/><br /><sub><b>Dropkiq</b></sub></a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://dailycontributors.com/"><img src="https://images.opencollective.com/dailycontributors/3c2e057/logo/256.png?height=100&width=100" width="100px;" alt="Dailycontributors"/><br /><sub><b>Dailycontributors</b></sub></a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/coni2k"><img src="https://avatars0.githubusercontent.com/u/1284601?v=4?s=100" width="100px;" alt="coni2k"/><br /><sub><b>Serkan Holat</b></sub></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/amit777"><img src="https://avatars0.githubusercontent.com/u/2703309?v=4?s=100" width="100px;" alt="amit777"/><br /><sub><b>amit777</b></sub></a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://opencollective.com/khaled-salem"><img src="https://images.opencollective.com/khaled-salem/avatar/256.png?height=256" width="100px;" alt="Khaled Salem"/><br /><sub><b>Khaled Salem</b></sub></a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://sentry.io/"><img src="https://avatars.githubusercontent.com/u/1396951?v=4?s=100" width="100px;" alt="Sentry"/><br /><sub><b>Sentry</b></sub></a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.checkoutblocks.com/"><img src="https://avatars.githubusercontent.com/u/114603307?v=4?s=100" width="100px;" alt="Checkout Blocks"/><br /><sub><b>Checkout Blocks</b></sub></a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- FINANCIAL-CONTRIBUTORS-END -->
|
||||
### Sponsors
|
||||
If LiquidJS is benefiting your business/company, please sponsor us to contribute to our effort!
|
||||
|
||||
<img src="https://opencollective.com/liquidjs/sponsors.svg?avatarHeight=72">
|
||||
|
||||
## Contributors ✨
|
||||
|
||||
@@ -147,7 +130,7 @@ Want to contribute? see [Contribution Guidelines][contribution]. Thanks goes to
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://twitter.com/IAmTrySound"><img src="https://avatars0.githubusercontent.com/u/5635476?v=4?s=100" width="100px;" alt="Bogdan Chadkin"/><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" valign="top" width="14.28%"><a href="https://hightouch.io"><img src="https://avatars0.githubusercontent.com/u/5959235?v=4?s=100" width="100px;" alt="Tejas Manohar"/><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" valign="top" width="14.28%"><a href="http://about.me/peterdehaan"><img src="https://avatars2.githubusercontent.com/u/557895?v=4?s=100" width="100px;" alt="Peter deHaan"/><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" valign="top" width="14.28%"><a href="https://github.com/amit777"><img src="https://avatars0.githubusercontent.com/u/2703309?v=4?s=100" width="100px;" alt="amit777"/><br /><sub><b>amit777</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=amit777" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/amit777"><img src="https://avatars0.githubusercontent.com/u/2703309?v=4?s=100" width="100px;" alt="amit777"/><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" valign="top" width="14.28%"><a href="http://www.ifi.uzh.ch/en/ce/people/schuldenzucker.html"><img src="https://avatars3.githubusercontent.com/u/1100776?v=4?s=100" width="100px;" alt="Steffen Schuldenzucker"/><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>
|
||||
@@ -187,13 +170,10 @@ Want to contribute? see [Contribution Guidelines][contribution]. Thanks goes to
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://sixtwothree.org"><img src="https://avatars.githubusercontent.com/u/73866?v=4?s=100" width="100px;" alt="Jason Garber"/><br /><sub><b>Jason Garber</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=jgarber623" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.checkoutblocks.com/"><img src="https://avatars.githubusercontent.com/u/114603307?v=4?s=100" width="100px;" alt="Checkout Blocks"/><br /><sub><b>Checkout Blocks</b></sub></a><br /><a href="#financial-Checkout-Blocks" title="Financial">💵</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.dropkiq.com/"><img src="https://avatars.githubusercontent.com/u/69064?v=4?s=100" width="100px;" alt="Adam Darrah"/><br /><sub><b>Adam Darrah</b></sub></a><br /><a href="#financial-akdarrah" title="Financial">💵</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.11ty.dev/"><img src="https://avatars.githubusercontent.com/u/35147177?v=4?s=100" width="100px;" alt="Eleventy"/><br /><sub><b>Eleventy</b></sub></a><br /><a href="#financial-11ty" title="Financial">💵</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://nickreilingh.com/"><img src="https://avatars.githubusercontent.com/u/2458645?v=4?s=100" width="100px;" alt="Nick Reilingh"/><br /><sub><b>Nick Reilingh</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=NReilingh" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://ebobby.org"><img src="https://avatars.githubusercontent.com/u/170356?v=4?s=100" width="100px;" alt="Francisco Soto"/><br /><sub><b>Francisco Soto</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=ebobby" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.davidlj95.com"><img src="https://avatars.githubusercontent.com/u/8050648?v=4?s=100" width="100px;" alt="David LJ"/><br /><sub><b>David LJ</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=davidlj95" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RasmusWL"><img src="https://avatars.githubusercontent.com/u/1054041?v=4?s=100" width="100px;" alt="Rasmus Wriedt Larsen"/><br /><sub><b>Rasmus Wriedt Larsen</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=RasmusWL" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/brunodccarvalho"><img src="https://avatars.githubusercontent.com/u/24962950?v=4?s=100" width="100px;" alt="Bruno Carvalho"/><br /><sub><b>Bruno Carvalho</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=brunodccarvalho" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fupengl"><img src="https://avatars.githubusercontent.com/u/20211964?v=4?s=100" width="100px;" alt="傅鹏"/><br /><sub><b>傅鹏</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=fupengl" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/joel-hamilton"><img src="https://avatars.githubusercontent.com/u/12899024?v=4?s=100" width="100px;" alt="Joel Hamilton"/><br /><sub><b>Joel Hamilton</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=joel-hamilton" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -210,3 +190,4 @@ Want to contribute? see [Contribution Guidelines][contribution]. Thanks goes to
|
||||
[github]: https://github.com/harttle/liquidjs
|
||||
[oc]: https://opencollective.com/liquidjs/
|
||||
[contribution]: https://liquidjs.com/tutorials/contribution-guidelines.html
|
||||
[financial-support]: https://liquidjs.com/tutorials/contribution-guidelines.html#Financial-Support
|
||||
|
||||
+12
-3
@@ -1,8 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Run `sed` in a way that's compatible with both macOS (BSD) and Linux (GNU)
|
||||
sedi() {
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
sed -i '' "$@"
|
||||
else
|
||||
sed -i "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
cd docs
|
||||
cp ../CHANGELOG.md source/tutorials/changelog.md
|
||||
sed -i \
|
||||
sedi \
|
||||
-e 's/{%/{% raw %}{%{% endraw %}/g' \
|
||||
-e 's/{{/{% raw %}{{{% endraw %}/g' \
|
||||
-e '1 s/"/\"/g' \
|
||||
@@ -11,7 +20,7 @@ sed -i \
|
||||
source/tutorials/changelog.md
|
||||
cp source/tutorials/changelog.md source/zh-cn/tutorials/changelog.md
|
||||
|
||||
sed -i -e '1i\
|
||||
sedi -e '1i\
|
||||
---\ntitle: Changelog\nauto: true\n---\n' source/tutorials/changelog.md
|
||||
sed -i -e '1i\
|
||||
sedi -e '1i\
|
||||
---\ntitle: 更新日志\nauto: true\n---\n' source/zh-cn/tutorials/changelog.md
|
||||
|
||||
@@ -9,7 +9,6 @@ sedi() {
|
||||
fi
|
||||
}
|
||||
|
||||
# create docs/themes/navy/layout/partial/all-contributors.swig
|
||||
cp .all-contributorsrc docs/.all-contributorsrc
|
||||
sedi \
|
||||
-e 's/README.md/docs\/themes\/navy\/layout\/partial\/all-contributors.swig/g' \
|
||||
@@ -18,11 +17,3 @@ sedi \
|
||||
|
||||
all-contributors --config docs/.all-contributorsrc generate
|
||||
sedi 's/<br \/>.*<\/td>/<\/a><\/td>/g' docs/themes/navy/layout/partial/all-contributors.swig
|
||||
|
||||
# create docs/themes/navy/layout/partial/financial-contributors.swig
|
||||
awk '/FINANCIAL-CONTRIBUTORS-BEGIN/{flag=1;next}/FINANCIAL-CONTRIBUTORS-END/{flag=0}flag' README.md | \
|
||||
sed 's/<br \/>.*<\/td>/<\/a><\/td>/g' | \
|
||||
sed 's/width="[^"]*"//g' | \
|
||||
tr -d '\n' | \
|
||||
sed 's/<\/tr>\s*<tr>//g' \
|
||||
> docs/themes/navy/layout/partial/financial-contributors.swig
|
||||
|
||||
Executable → Regular
+4
-12
@@ -4,19 +4,18 @@ 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 isWord = c => '_-?'.includes(c) || isCharacter(c) || isNumber(c)
|
||||
const isBlank = c => '\n\t \r\v\f'.includes(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 === '+'
|
||||
// See https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/RegExp
|
||||
const unicodeBlanks = '\u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000'
|
||||
const unicodePunctuations = '“”'
|
||||
|
||||
const types = []
|
||||
for (let i = 0; i < 128; i++) {
|
||||
const c = String.fromCharCode(i)
|
||||
let n = 0
|
||||
if (isWord(c)) n |= 1
|
||||
if (isIdentifier(c)) n |= 1
|
||||
if (isOperator(c)) n |= 2
|
||||
if (isBlank(c)) n |= 4
|
||||
if (isQuote(c)) n |= 8
|
||||
@@ -32,20 +31,13 @@ console.log(`
|
||||
// This file is generated by bin/character-gen.js
|
||||
// bitmask character types to boost performance
|
||||
export const TYPES = [${types.join(', ')}]
|
||||
export const WORD = 1
|
||||
export const IDENTIFIER = 1
|
||||
export const OPERATOR = 2
|
||||
export const BLANK = 4
|
||||
export const QUOTE = 8
|
||||
export const INLINE_BLANK = 16
|
||||
export const NUMBER = 32
|
||||
export const SIGN = 64
|
||||
export const PUNCTUATION = 128
|
||||
|
||||
export function isWord (char: string): boolean {
|
||||
const code = char.charCodeAt(0)
|
||||
return code >= 128 ? !TYPES[code] : !!(TYPES[code] & WORD)
|
||||
}
|
||||
`.trim())
|
||||
|
||||
console.log([...unicodeBlanks].map(char => `TYPES[${char.charCodeAt(0)}]`).join(' = ') + ' = BLANK')
|
||||
console.log([...unicodePunctuations].map(char => `TYPES[${char.charCodeAt(0)}]`).join(' = ') + ' = PUNCTUATION')
|
||||
|
||||
+2
-2
@@ -1,11 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
VERSION_LATEST=$(cat package.json | grep '"version":' | head -1 | awk -F'"' '{print $4}')
|
||||
VERSION_LATEST=$(cat package.json | grep '"version":' | awk -F'"' '{print $4}')
|
||||
FILE_LOCAL=dist/liquid.node.cjs.js
|
||||
FILE_LATEST=dist/liquid.node.cjs.$VERSION_LATEST.js
|
||||
URL_LATEST=https://unpkg.com/liquidjs@$VERSION_LATEST/dist/liquid.node.cjs.js
|
||||
|
||||
if [ ! -f "$FILE_LATEST" ]; then
|
||||
if [ ! -f $FILE_LATEST ]; then
|
||||
curl $URL_LATEST > $FILE_LATEST
|
||||
fi
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ tutorials:
|
||||
access_scope_in_filters: access-scope-in-filters.html
|
||||
parse_parameters: parse-parameters.html
|
||||
render_tag_content: render-tag-content.html
|
||||
drops: drops.html
|
||||
sync_and_async: sync-and-async.html
|
||||
whitespace: whitespace-control.html
|
||||
plugins: plugins.html
|
||||
@@ -70,7 +69,6 @@ filters:
|
||||
strip: strip.html
|
||||
strip_html: strip_html.html
|
||||
strip_newlines: strip_newlines.html
|
||||
sum: sum.html
|
||||
times: times.html
|
||||
truncate: truncate.html
|
||||
truncatewords: truncatewords.html
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
---
|
||||
title: sum
|
||||
---
|
||||
|
||||
{% since %}v10.10.0{% endsince %}
|
||||
|
||||
Computes the sum of all the numbers in an array.
|
||||
An optional argument specifies which property of the array's items to sum up.
|
||||
|
||||
In this example, assume the object `cart.products` contains an array of all products in the cart of a website.
|
||||
Assume each cart product has a `qty` property that gives the count of that product instance in the cart.
|
||||
Using the `sum` filter we can calculate the total number of products in the cart.
|
||||
|
||||
Input
|
||||
```liquid
|
||||
The cart has {{ order.products | sum: "qty" }} products.
|
||||
```
|
||||
|
||||
Output
|
||||
```text
|
||||
The cart has 7 products.
|
||||
```
|
||||
@@ -20,7 +20,17 @@ Getting started and building is described in [CONTRIBUTING.md](https://github.co
|
||||
|
||||
## Financial Support
|
||||
|
||||
LiquidJS is Open Source and Free. To help it live and thrive, especially when LiquidJS is benefiting your business, please consider contribute on [Github Sponsors](https://github.com/sponsors/harttle) or [Open Collective][oc]. If I'm missing anything, find me via Twitter (harttleharttle) or email (harttleharttle at gmail), to add you into [the contributors table](https://github.com/harttle/liquidjs#contributors-).
|
||||
LiquidJS is Open Source and Free. To help it live and thrive, you can contribute on [Open Collective][oc] or [Github Sponsors](https://github.com/sponsors/harttle). Please find me via Twitter (harttleharttle) or email (harttleharttle at gmail), to add you into [the contributors table](https://github.com/harttle/liquidjs#contributors-).
|
||||
|
||||
### Backers
|
||||
If you personally like LiquidJS and find it's useful to you, you can become a backer!
|
||||
|
||||
<object type="image/svg+xml" data="https://opencollective.com/liquidjs/backers.svg?avatarHeight=72"></object>
|
||||
|
||||
### Sponsors
|
||||
If LiquidJS is benefiting your business/company, please sponsor us to make it better!
|
||||
|
||||
<object type="image/svg+xml" data="https://opencollective.com/liquidjs/sponsors.svg?avatarHeight=72"></object>
|
||||
|
||||
[oc]: https://opencollective.com/liquidjs/contribute/backer-10665/checkout
|
||||
[shopify/liquid]: https://shopify.github.io/liquid/
|
||||
|
||||
@@ -1,201 +0,0 @@
|
||||
---
|
||||
title: Liquid Drops
|
||||
---
|
||||
|
||||
LiquidJS also provides a mechanism similar to [Shopify Drops][shopify-drops], allowing template authors to incorporate custom functionality in resolving variable values.
|
||||
|
||||
{% note info Drop for JavaScript %}
|
||||
<em>Drop</em> interface is implemented differently in LiquidJS compared to built-in filters and other template functionalities. Since LiquidJS runs in JavaScript, custom Drops need to be reimplemented in JavaScript anyway. There's no compatibility between JavaScript classes and Ruby classes.
|
||||
{% endnote %}
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```javascript
|
||||
import { Liquid, Drop } from 'liquidjs'
|
||||
|
||||
class SettingsDrop extends Drop {
|
||||
constructor() {
|
||||
super()
|
||||
this.foo = 'FOO'
|
||||
}
|
||||
bar() {
|
||||
return 'BAR'
|
||||
}
|
||||
}
|
||||
|
||||
const engine = new Liquid()
|
||||
const template = `foo: {{settings.foo}}, bar: {{settings.bar}}`
|
||||
const context = { settings: new SettingsDrop() }
|
||||
// Outputs: "foo: FOO, bar: BAR"
|
||||
engine.parseAndRender(template, context).then(html => console.log(html))
|
||||
```
|
||||
|
||||
[Runkit link](https://runkit.com/embed/2is7di4mc7kk)
|
||||
|
||||
As shown above, besides reading properties from context scopes, you can also call methods. You only need to create a custom class inherited from `Drop`.
|
||||
|
||||
{% note tip Async Methods %}
|
||||
LiquidJS is fully async-friendly. You can safely return a Promise in your Drop methods or define your methods in Drop as `async`.
|
||||
{% endnote %}
|
||||
|
||||
## liquidMethodMissing
|
||||
|
||||
For cases when there isn't a fixed set of properties, you can leverage `liquidMethodMissing` to dynamically resolve the value of a variable name.
|
||||
|
||||
```javascript
|
||||
import { Liquid, Drop } from 'liquidjs'
|
||||
|
||||
class SettingsDrop extends Drop {
|
||||
liquidMethodMissing(key) {
|
||||
return key.toUpperCase()
|
||||
}
|
||||
}
|
||||
|
||||
const engine = new Liquid()
|
||||
// Outputs: "COO"
|
||||
engine.parseAndRender("{{settings.coo}}", { settings: new SettingsDrop() })
|
||||
.then(html => console.log(html))
|
||||
```
|
||||
|
||||
`liquidMethodMissing` supports Promise, meaning you can make async calls within it. A more useful case can be fetching the value dynamically from the database. By using Drops, you can avoid hardcoding each property into the context. For example:
|
||||
|
||||
```javascript
|
||||
import { Liquid, Drop } from 'liquidjs'
|
||||
|
||||
class DBDrop extends Drop {
|
||||
async liquidMethodMissing(key) {
|
||||
const record = await db.getRecordByKey(key)
|
||||
return record.value
|
||||
}
|
||||
}
|
||||
|
||||
const engine = new Liquid()
|
||||
const context = { db: new DBDrop() }
|
||||
engine.parseAndRender("{{db.coo}}", context).then(html => console.log(html))
|
||||
```
|
||||
|
||||
## valueOf
|
||||
|
||||
Drops can implement a `valueOf()` method, the return value of which can be used to replace itself in the output. For example:
|
||||
|
||||
```javascript
|
||||
import { Liquid, Drop } from 'liquidjs'
|
||||
|
||||
class ColorDrop extends Drop {
|
||||
valueOf() {
|
||||
return 'red'
|
||||
}
|
||||
}
|
||||
|
||||
const engine = new Liquid()
|
||||
const context = { color: new ColorDrop() }
|
||||
// Outputs: "red"
|
||||
engine.parseAndRender("{{color}}", context).then(html => console.log(html))
|
||||
```
|
||||
|
||||
## toLiquid
|
||||
|
||||
`toLiquid()` is not a method of `Drop`, but it can be used to return a `Drop`. In cases where you have a fixed structure in the `context` that cannot change its values, you can implement `toLiquid()` to let LiquidJS use the returned value instead of itself to render the templates.
|
||||
|
||||
```javascript
|
||||
import { Liquid, Drop } from 'liquidjs'
|
||||
|
||||
const context = {
|
||||
person: {
|
||||
firstName: "Jun",
|
||||
lastName: "Yang",
|
||||
name: "Jun Yang",
|
||||
toLiquid: () => ({
|
||||
firstName: this.firstName,
|
||||
lastName: this.lastName,
|
||||
// use a different `name`
|
||||
name: "Yang, Jun"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const engine = new Liquid()
|
||||
// Outputs: "Yang, Jun"
|
||||
engine.parseAndRender("{{person.name}}", context).then(html => console.log(html))
|
||||
```
|
||||
|
||||
Of course, you can also return a `PersonDrop` instance in the `toLiquid()` method and implement this functionality within `PersonDrop`:
|
||||
|
||||
```javascript
|
||||
import { Liquid, Drop } from 'liquidjs'
|
||||
|
||||
class PersonDrop extends Drop {
|
||||
constructor(person) {
|
||||
super()
|
||||
this.person = person
|
||||
}
|
||||
name() {
|
||||
return this.person.lastName + ", " + this.person.firstName
|
||||
}
|
||||
}
|
||||
|
||||
const context = {
|
||||
person: {
|
||||
firstName: "Jun",
|
||||
lastName: "Yang",
|
||||
name: "Jun Yang",
|
||||
toLiquid: function () { return new PersonDrop(this) }
|
||||
}
|
||||
}
|
||||
|
||||
const engine = new Liquid()
|
||||
// Outputs: "Yang, Jun"
|
||||
engine.parseAndRender("{{person.name}}", context).then(html => console.log(html))
|
||||
```
|
||||
|
||||
{% note info <code>toLiquid()</code> vs. <code>valueOf()</code> Difference %}
|
||||
<ul>
|
||||
<li><code>valueOf()</code> is typically used to define how the current variable should be rendered, while <code>toLiquid()</code> is often used to convert an object into a Drop or another scope provided to the template.</li>
|
||||
<li><code>valueOf()</code> is a method exclusive to Drops; whereas <code>toLiquid()</code> can be used on any scope object.</li>
|
||||
<li><code>valueOf()</code> is called when the variable itself is about to be rendered, replacing itself; whereas <code>toLiquid()</code> is called when its properties are about to be read.</li>
|
||||
</ul>
|
||||
{% endnote %}
|
||||
|
||||
## Special Drops
|
||||
|
||||
LiquidJS itself implements several built-in drops to facilitate template writing. This part is compatible with Shopify Liquid, as we need templates to be portable.
|
||||
|
||||
### blank
|
||||
|
||||
Useful to check whether a string variable is `false`, `null`, `undefined`, an empty string, or a string containing only blank characters.
|
||||
|
||||
```liquid
|
||||
{% unless author == blank %}
|
||||
{{author}}
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
### empty
|
||||
|
||||
Useful to check if an array, string, or object is empty.
|
||||
|
||||
```liquid
|
||||
{% if authors == empty %}
|
||||
Author list is empty
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
{% note info <code>empty</code> implementation %}
|
||||
For arrays and strings, LiquidJS checks their `.length` property. For objects, LiquidJS calls `Object.keys()` to check whether they have keys.
|
||||
{% endnote %}
|
||||
|
||||
### nil
|
||||
|
||||
`nil` Drop is used to check whether a variable is not defined or defined as `null` or `undefined`, essentially equivalent to JavaScript `== null` check.
|
||||
|
||||
```liquid
|
||||
{% if notexist == nil %}
|
||||
null variable
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
### Other Drops
|
||||
|
||||
There are still several Drops for specific tags, like `forloop`, `tablerowloop`, `block`, which are covered by respective tag documents.
|
||||
|
||||
[shopify-drops]: https://github.com/Shopify/liquid/wiki/Introduction-to-Drops
|
||||
@@ -60,7 +60,7 @@ engine.registerTag('random', {
|
||||
|
||||
Calling this tag in scope `{ bar: "bar", obj: { coo: "coo" } }` yields exactly the same result as the first example. See this JSFiddle: <http://jsfiddle.net/ctj364up/3/>
|
||||
|
||||
{% note info Async and Promises %}
|
||||
{% note info Async ans Promises %}
|
||||
Async calls in LiquidJS are implemented by generators directly, for we can call generators in synchronous manner so this tag implementation is also valid for `renderSync()`, `parseAndRenderSync()`, `renderFileSync()`. If you need to await a promise in tag implementation, simply replace `await somePromise` with `yield somePromise` and keep `* render()` instead of `async render()` will do the trick. See <a href="/tutorials/sync-and-async.html">Sync and Async</a> for more details.
|
||||
{% endnote %}
|
||||
|
||||
|
||||
@@ -100,4 +100,4 @@ Most of the [options available through the JavaScript API][options] are also ava
|
||||
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
|
||||
[options]: ./options.html
|
||||
[options]: ./options.md
|
||||
|
||||
@@ -22,6 +22,16 @@ Star 是支持 LiquidJS 最重要的方式,也是最简单的方式:通过
|
||||
|
||||
LiquidJS 是开源和免费的,但支持 [Open Collective][oc] 和 [Github Sponsors](https://github.com/sponsors/harttle) 赞助,请通过 Twitter (harttleharttle) 或邮件 (harttleharttle at gmail) 联系我,把您加到 [贡献者列表](https://github.com/harttle/liquidjs#contributors-) 中。
|
||||
|
||||
### 支持者
|
||||
如果你喜欢 LiquidJS,希望支持我们,你可以成为 Open Collective Backer!
|
||||
|
||||
<object type="image/svg+xml" data="https://opencollective.com/liquidjs/backers.svg?avatarHeight=72"></object>
|
||||
|
||||
### 赞助商
|
||||
如果你的公司/业务在使用 LiquidJS,请考虑赞助我们!让 LiquidJS 变得更好,以及保持开放和免费!
|
||||
|
||||
<object type="image/svg+xml" data="https://opencollective.com/liquidjs/sponsors.svg?avatarHeight=72"></object>
|
||||
|
||||
[oc]: https://opencollective.com/liquidjs/
|
||||
[shopify/liquid]: https://shopify.github.io/liquid/
|
||||
[caniuse-promises]: http://caniuse.com/#feat=promises
|
||||
|
||||
@@ -1,201 +0,0 @@
|
||||
---
|
||||
title: Liquid Drop
|
||||
---
|
||||
|
||||
LiquidJS 还提供了一种类似于 [Shopify Drop][shopify-drops] 的机制,用于为模板作者提供在自定义解析变量值的功能。
|
||||
|
||||
{% note info JavaScript 中的 Drop %}
|
||||
<em>Drop</em> 接口在 LiquidJS 中实现方式与内置过滤器和其他模板功能不同。由于 LiquidJS 在 JavaScript 中运行,自定义 Drop 在 JavaScript 中一定需要重新实现。JavaScript 类与 Ruby 类之间没有兼容性可言。
|
||||
{% endnote %}
|
||||
|
||||
## 基本用法
|
||||
|
||||
```javascript
|
||||
import { Liquid, Drop } from 'liquidjs'
|
||||
|
||||
class SettingsDrop extends Drop {
|
||||
constructor() {
|
||||
super()
|
||||
this.foo = 'FOO'
|
||||
}
|
||||
bar() {
|
||||
return 'BAR'
|
||||
}
|
||||
}
|
||||
|
||||
const engine = new Liquid()
|
||||
const template = `foo: {{settings.foo}}, bar: {{settings.bar}}`
|
||||
const context = { settings: new SettingsDrop() }
|
||||
// 输出: "foo: FOO, bar: BAR"
|
||||
engine.parseAndRender(template, context).then(html => console.log(html))
|
||||
```
|
||||
|
||||
[Runkit 链接](https://runkit.com/embed/2is7di4mc7kk)
|
||||
|
||||
如上所示,除了从上下文作用域中读取属性外,还可以调用方法。您只需创建一个继承自 `Drop` 的自定义类。
|
||||
|
||||
{% note tip 异步方法 %}
|
||||
LiquidJS 完全支持异步,您可以在 Drop 的方法中安全地返回 Promise,或将 Drop 的方法定义为 `async`。
|
||||
{% endnote %}
|
||||
|
||||
## liquidMethodMissing
|
||||
|
||||
如果属性名不能静态地确定的情况下,可以利用 `liquidMethodMissing` 来动态解析变量的值。
|
||||
|
||||
```javascript
|
||||
import { Liquid, Drop } from 'liquidjs'
|
||||
|
||||
class SettingsDrop extends Drop {
|
||||
liquidMethodMissing(key) {
|
||||
return key.toUpperCase()
|
||||
}
|
||||
}
|
||||
|
||||
const engine = new Liquid()
|
||||
// 输出: "COO"
|
||||
engine.parseAndRender("{{settings.coo}}", { settings: new SettingsDrop() })
|
||||
.then(html => console.log(html))
|
||||
```
|
||||
|
||||
`liquidMethodMissing` 支持 Promise,这意味着您可以在其中进行异步调用。一个更有用的例子是通过使用 Drop 动态地从数据库获取值。通过使用 Drop,您可以避免将每个属性都硬编码到上下文中。例如:
|
||||
|
||||
```javascript
|
||||
import { Liquid, Drop } from 'liquidjs'
|
||||
|
||||
class DBDrop extends Drop {
|
||||
async liquidMethodMissing(key) {
|
||||
const record = await db.getRecordByKey(key)
|
||||
return record.value
|
||||
}
|
||||
}
|
||||
|
||||
const engine = new Liquid()
|
||||
const context = { db: new DBDrop() }
|
||||
engine.parseAndRender("{{db.coo}}", context).then(html => console.log(html))
|
||||
```
|
||||
|
||||
## valueOf
|
||||
|
||||
Drop 可以实现一个 `valueOf()` 方法,用于在输出中替换自身。例如:
|
||||
|
||||
```javascript
|
||||
import { Liquid, Drop } from 'liquidjs'
|
||||
|
||||
class ColorDrop extends Drop {
|
||||
valueOf() {
|
||||
return 'red'
|
||||
}
|
||||
}
|
||||
|
||||
const engine = new Liquid()
|
||||
const context = { color: new ColorDrop() }
|
||||
// 输出: "red"
|
||||
engine.parseAndRender("{{color}}", context).then(html => console.log(html))
|
||||
```
|
||||
|
||||
## toLiquid
|
||||
|
||||
`toLiquid()` 不是 `Drop` 的方法,但它可以用于返回一个 `Drop`。在您有一个上下文中固定结构且不能更改其值的情况下,您可以实现 `toLiquid()`,以便让 LiquidJS 使用返回的值而不是自身来渲染模板。
|
||||
|
||||
```javascript
|
||||
import { Liquid, Drop } from 'liquidjs'
|
||||
|
||||
const context = {
|
||||
person: {
|
||||
firstName: "Jun",
|
||||
lastName: "Yang",
|
||||
name: "Jun Yang",
|
||||
toLiquid: () => ({
|
||||
firstName: this.firstName,
|
||||
lastName: this.lastName,
|
||||
// 使用不同的 `name`
|
||||
name: "Yang, Jun"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const engine = new Liquid()
|
||||
// 输出: "Yang, Jun"
|
||||
engine.parseAndRender("{{person.name}}", context).then(html => console.log(html))
|
||||
```
|
||||
|
||||
当然,您还可以在 `toLiquid()` 方法中返回一个 `PersonDrop` 实例,并在 `PersonDrop` 中实现此功能:
|
||||
|
||||
```javascript
|
||||
import { Liquid, Drop } from 'liquidjs'
|
||||
|
||||
class PersonDrop extends Drop {
|
||||
constructor(person) {
|
||||
super()
|
||||
this.person = person
|
||||
}
|
||||
name() {
|
||||
return this.person.lastName + ", " + this.person.firstName
|
||||
}
|
||||
}
|
||||
|
||||
const context = {
|
||||
person: {
|
||||
firstName: "Jun",
|
||||
lastName: "Yang",
|
||||
name: "Jun Yang",
|
||||
toLiquid: function () { return new PersonDrop(this) }
|
||||
}
|
||||
}
|
||||
|
||||
const engine = new Liquid()
|
||||
// 输出: "Yang, Jun"
|
||||
engine.parseAndRender("{{person.name}}", context).then(html => console.log(html))
|
||||
```
|
||||
|
||||
{% note info <code>toLiquid()</code> 和 <code>valueOf()</code> 的区别 %}
|
||||
<ul>
|
||||
<li><code>valueOf()</code> 通常用来定义当前变量如何渲染,<code>toLiquid()</code> 通常用来把一个对象转换为 Drop 或另一个提供给模板的 scope。</li>
|
||||
<li><code>valueOf()</code> 是 Drop 才有的方法;而 <code>toLiquid()</code> 可以用在任何 scope 对象上。</li>
|
||||
<li><code>valueOf()</code> 是在自己即将被渲染时,用来替代自己;而 <code>toLiquid()</code> 在即将读取它的属性时才会被调用。</li>
|
||||
</ul>
|
||||
{% endnote %}
|
||||
|
||||
## 特殊 Drop
|
||||
|
||||
LiquidJS 本身实现了几个内置 Drop,以促进模板编写。此部分与 Shopify Liquid 兼容,因为我们需要模板具有可移植性。
|
||||
|
||||
### blank
|
||||
|
||||
用于检查字符串变量是否为 `false`、`null`、`undefined`、空字符串或字符串仅包含空白字符。
|
||||
|
||||
```liquid
|
||||
{% unless author == blank %}
|
||||
{{author}}
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
### empty
|
||||
|
||||
用于检查数组、字符串或对象是否为空。
|
||||
|
||||
```liquid
|
||||
{% if authors == empty %}
|
||||
作者列表为空
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
{% note info <code>empty</code> 的实现 %}
|
||||
对于数组和字符串,LiquidJS 检查它们的 `.length` 属性。对于对象,LiquidJS 调用 `Object.keys()` 来检查它是否有键。
|
||||
{% endnote %}
|
||||
|
||||
### nil
|
||||
|
||||
`nil` Drop 用于检查变量是否未定义或定义为 `null` 或 `undefined`,本质上等同于 JavaScript 的 `== null` 检查。
|
||||
|
||||
```liquid
|
||||
{% if notexist == nil %}
|
||||
空变量
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
### 其他 Drop
|
||||
|
||||
仍然有一些特定标签的 Drop,例如 `forloop`、`tablerowloop`、`block`,这些在各自的标签文档中有详细介绍。
|
||||
|
||||
[shopify-drops]: https://github.com/Shopify/liquid/wiki/Introduction-to-Drops
|
||||
Vendored
+4
-2
@@ -11,9 +11,12 @@ index:
|
||||
contributors:
|
||||
title: Contributors
|
||||
description: 'LiquidJS follows the <a href="https://github.com/all-contributors/all-contributors">all-contributors</a> specification, see <a href="tutorials/contribution-guidelines.html">guildelines here</a>! Thanks goes to these wonderful people:'
|
||||
backers:
|
||||
title: Backers
|
||||
description: 'Love LiquidJS and want to contribute? <a href="https://opencollective.com/liquidjs">Become a backer</a>. Find out more <a href="tutorials/contribution-guidelines.html#Financial-Support">how to contribute</a>!'
|
||||
sponsors:
|
||||
title: Sponsors
|
||||
description: 'If you personally love LiquidJS or it's benefiting your business, please <a href="https://github.com/sponsors/harttle">sponsor us</a>!'
|
||||
description: 'If LiquidJS is benefiting your business/company, please <a href="https://opencollective.com/liquidjs">sponsor us</a> to make it better!'
|
||||
|
||||
playground:
|
||||
title: Playground
|
||||
@@ -44,7 +47,6 @@ sidebar:
|
||||
access_scope_in_filters: Access Scope in Filters
|
||||
parse_parameters: Parse Parameters
|
||||
render_tag_content: Render Tag Content
|
||||
drops: Liquid Drops
|
||||
sync_and_async: Sync and Async
|
||||
whitespace: Whitespace Control
|
||||
plugins: Plugins
|
||||
|
||||
Vendored
-1
@@ -44,7 +44,6 @@ sidebar:
|
||||
access_scope_in_filters: 过滤器里访问上下文
|
||||
parse_parameters: 参数解析
|
||||
render_tag_content: 渲染标签内容
|
||||
drops: Liquid Drop
|
||||
sync_and_async: 同步和异步
|
||||
whitespace: 换行和缩进
|
||||
plugins: 插件
|
||||
|
||||
Vendored
+11
-4
@@ -37,20 +37,27 @@
|
||||
<div class="inner">
|
||||
<h3>{{__('index.contributors.title')}}</h3>
|
||||
<p class="description">{{__('index.contributors.description')}}</p>
|
||||
<div class="contributors">
|
||||
<div class="all-contributors">
|
||||
{{ partial('partial/all-contributors') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="backers-wrap">
|
||||
<div class="wrapper">
|
||||
<div class="inner">
|
||||
<h3>{{__('index.backers.title')}}</h3>
|
||||
<p class="description">{{__('index.backers.description')}}</p>
|
||||
<img src="https://opencollective.com/liquidjs/individuals.svg">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="sponsors-wrap">
|
||||
<div class="wrapper">
|
||||
<div class="inner">
|
||||
<h3>{{__('index.sponsors.title')}}</h3>
|
||||
<p class="description">{{__('index.sponsors.description')}}</p>
|
||||
<div class="contributors">
|
||||
{{ partial('partial/financial-contributors') }}
|
||||
</div>
|
||||
<img class="open-collective" src="https://opencollective.com/liquidjs/organizations.svg">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -68,13 +68,6 @@
|
||||
<td align="center" valign="top" width="0%"><a href="https://github.com/mahyar-pasarzangene"><img src="https://avatars.githubusercontent.com/u/16485039?v=4?s=100" width="100px;" alt="Mahyar Pasarzangene"/></a></td>
|
||||
<td align="center" valign="top" width="0%"><a href="https://hubelbauer.net/"><img src="https://avatars.githubusercontent.com/u/6831144?v=4?s=100" width="100px;" alt="Tomáš Hübelbauer"/></a></td>
|
||||
<td align="center" valign="top" width="0%"><a href="https://sixtwothree.org"><img src="https://avatars.githubusercontent.com/u/73866?v=4?s=100" width="100px;" alt="Jason Garber"/></a></td>
|
||||
<td align="center" valign="top" width="0%"><a href="http://nickreilingh.com/"><img src="https://avatars.githubusercontent.com/u/2458645?v=4?s=100" width="100px;" alt="Nick Reilingh"/></a></td>
|
||||
<td align="center" valign="top" width="0%"><a href="http://ebobby.org"><img src="https://avatars.githubusercontent.com/u/170356?v=4?s=100" width="100px;" alt="Francisco Soto"/></a></td>
|
||||
<td align="center" valign="top" width="0%"><a href="https://www.davidlj95.com"><img src="https://avatars.githubusercontent.com/u/8050648?v=4?s=100" width="100px;" alt="David LJ"/></a></td>
|
||||
<td align="center" valign="top" width="0%"><a href="https://github.com/RasmusWL"><img src="https://avatars.githubusercontent.com/u/1054041?v=4?s=100" width="100px;" alt="Rasmus Wriedt Larsen"/></a></td>
|
||||
<td align="center" valign="top" width="0%"><a href="https://github.com/brunodccarvalho"><img src="https://avatars.githubusercontent.com/u/24962950?v=4?s=100" width="100px;" alt="Bruno Carvalho"/></a></td>
|
||||
<td align="center" valign="top" width="0%"><a href="https://github.com/fupengl"><img src="https://avatars.githubusercontent.com/u/20211964?v=4?s=100" width="100px;" alt="傅鹏"/></a></td>
|
||||
<td align="center" valign="top" width="0%"><a href="https://github.com/joel-hamilton"><img src="https://avatars.githubusercontent.com/u/12899024?v=4?s=100" width="100px;" alt="Joel Hamilton"/></a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<table> <tbody> <tr> <td align="center" valign="top" ><a href="https://www.opensense.com/"><img src="https://images.opencollective.com/opensense-inc/bf840ae/logo/256.png?height=100" alt="Opensense Inc."/></a></td> <td align="center" valign="top" ><a href="https://www.11ty.dev/"><img src="https://avatars.githubusercontent.com/u/35147177?v=4?s=100" alt="Eleventy"/></a></td> <td align="center" valign="top" ><a href="http://about.me/peterdehaan"><img src="https://avatars2.githubusercontent.com/u/557895?v=4?s=100" alt="Peter deHaan"/></a></td> <td align="center" valign="top" ><a href="https://opencollective.com/touchless"><img src="https://images.opencollective.com/touchless/273bc74/logo/256.png?height=100" alt="Touchless"/></a></td> <td align="center" valign="top" ><a href="https://www.dropkiq.com/"><img src="https://images.opencollective.com/1bertlol/43a8ea8/logo/256.png?height=100" alt="Adam Darrah"/></a></td> <td align="center" valign="top" ><a href="https://dailycontributors.com/"><img src="https://images.opencollective.com/dailycontributors/3c2e057/logo/256.png?height=100&width=100" alt="Dailycontributors"/></a></td> <td align="center" valign="top" ><a href="https://github.com/coni2k"><img src="https://avatars0.githubusercontent.com/u/1284601?v=4?s=100" alt="coni2k"/></a></td> <td align="center" valign="top" ><a href="https://github.com/amit777"><img src="https://avatars0.githubusercontent.com/u/2703309?v=4?s=100" alt="amit777"/></a></td> <td align="center" valign="top" ><a href="https://opencollective.com/khaled-salem"><img src="https://images.opencollective.com/khaled-salem/avatar/256.png?height=256" alt="Khaled Salem"/></a></td> <td align="center" valign="top" ><a href="https://sentry.io/"><img src="https://avatars.githubusercontent.com/u/1396951?v=4?s=100" alt="Sentry"/></a></td> <td align="center" valign="top" ><a href="https://www.checkoutblocks.com/"><img src="https://avatars.githubusercontent.com/u/114603307?v=4?s=100" alt="Checkout Blocks"/></a></td> </tr> </tbody></table>
|
||||
+1
@@ -19,6 +19,7 @@
|
||||
</select>
|
||||
</div>
|
||||
<a href="https://github.com/{{ config.github }}" class="main-nav-link icon-nav-link"><i class="icon-github"></i> <span class="icon-nav-title">Github</span></a>
|
||||
<a href="https://opencollective.com/liquidjs" class="main-nav-link icon-nav-link"><i class="icon-opencollective"></i> <span class="icon-nav-title">Support</span></a>
|
||||
<a id="mobile-nav-toggle">
|
||||
<span class="mobile-nav-toggle-bar"></span>
|
||||
<span class="mobile-nav-toggle-bar"></span>
|
||||
|
||||
+18
-20
@@ -171,7 +171,7 @@
|
||||
background: var(--color-link-hover)
|
||||
color: #fff
|
||||
|
||||
#sponsors-wrap, #contributors-wrap
|
||||
#backers-wrap, #sponsors-wrap, #contributors-wrap
|
||||
background: var(--color-navy-lighter)
|
||||
border-top: 1px solid #161d24
|
||||
border-bottom: 1px solid #161d24
|
||||
@@ -192,20 +192,28 @@
|
||||
text-decoration: none
|
||||
|
||||
#contributors-wrap
|
||||
.all-contributors
|
||||
tr
|
||||
display: flex
|
||||
flex-wrap: wrap
|
||||
td
|
||||
margin: 10px 2px 0
|
||||
a img
|
||||
display: inline-block
|
||||
height: 72px
|
||||
width: 72px
|
||||
background: var(--color-gray)
|
||||
border-radius: 50%
|
||||
margin-bottom: 10px
|
||||
.description a
|
||||
color: #fff
|
||||
&:hover
|
||||
background: #fff
|
||||
color: var(--color-navy-lighter)
|
||||
|
||||
#sponsors-wrap
|
||||
#sponsors-wrap, #backers-wrap
|
||||
border: none
|
||||
text-align: right
|
||||
background: var(--color-content-bg)
|
||||
overflow: hidden;
|
||||
.contributors
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
.inner
|
||||
h3
|
||||
color: var(--color-default)
|
||||
@@ -227,16 +235,6 @@
|
||||
background: #fff
|
||||
color: var(--color-navy-lighter)
|
||||
|
||||
.contributors
|
||||
tr
|
||||
display: flex
|
||||
flex-wrap: wrap
|
||||
td
|
||||
margin: 10px 2px 0
|
||||
a img
|
||||
display: inline-block
|
||||
height: 72px
|
||||
width: 72px
|
||||
background: var(--color-gray)
|
||||
border-radius: 50%
|
||||
margin-bottom: 10px
|
||||
#backers-wrap
|
||||
text-align: right
|
||||
background: var(--color-content-bg)
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
note-tip = #0fff00
|
||||
note-tip = hsl(40, 100%, 50%)
|
||||
note-info = hsl(200, 100%, 50%)
|
||||
note-warn = hsl(0, 100%, 50%)
|
||||
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "liquidjs",
|
||||
"version": "10.10.2",
|
||||
"version": "10.8.4",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
+2
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "liquidjs",
|
||||
"version": "10.10.2",
|
||||
"version": "10.8.4",
|
||||
"description": "A simple, expressive and safe Shopify / Github Pages compatible template engine in pure JavaScript.",
|
||||
"main": "dist/liquid.node.cjs.js",
|
||||
"module": "dist/liquid.node.esm.js",
|
||||
@@ -9,7 +9,7 @@
|
||||
"./dist/liquid.node.cjs.js": "./dist/liquid.browser.umd.js",
|
||||
"./dist/liquid.node.esm.js": "./dist/liquid.browser.esm.js"
|
||||
},
|
||||
"types": "dist/index.d.ts",
|
||||
"types": "dist/src/index.d.ts",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
@@ -21,12 +21,10 @@
|
||||
"test:e2e": "jest test/e2e",
|
||||
"perf:diff": "bin/perf-diff.sh",
|
||||
"perf:engines": "cd benchmark && npm run engines",
|
||||
"version": "npm run build && npm test",
|
||||
"build": "rollup -c rollup.config.mjs",
|
||||
"build:cjs": "BUNDLES=cjs rollup -c rollup.config.mjs",
|
||||
"build:min": "BUNDLES=min rollup -c rollup.config.mjs",
|
||||
"build:umd": "BUNDLES=umd rollup -c rollup.config.mjs",
|
||||
"build:charmap": "./bin/character-gen.js > src/util/character.ts",
|
||||
"build:docs": "bin/build-docs.sh"
|
||||
},
|
||||
"bin": {
|
||||
|
||||
+3
-4
@@ -18,12 +18,11 @@ const treeshake = {
|
||||
const tsconfig = (target) => ({
|
||||
check: true,
|
||||
tsconfigOverride: {
|
||||
include: ['src'],
|
||||
exclude: ['test', 'benchmark'],
|
||||
include: [ 'src' ],
|
||||
exclude: [ 'test', 'benchmark' ],
|
||||
compilerOptions: {
|
||||
target,
|
||||
module: 'ES2015',
|
||||
rootDir: 'src'
|
||||
module: 'ES2015'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Drop } from '../drop/drop'
|
||||
import { __assign } from 'tslib'
|
||||
import { NormalizedFullOptions, defaultOptions, RenderOptions } from '../liquid-options'
|
||||
import { Scope } from './scope'
|
||||
import { isArray, isNil, isUndefined, isString, isFunction, toLiquid, InternalUndefinedVariableError, toValueSync } from '../util'
|
||||
import { isArray, isNil, isString, isFunction, toLiquid, InternalUndefinedVariableError, toValueSync } from '../util'
|
||||
|
||||
type PropertyKey = string | number;
|
||||
|
||||
@@ -76,11 +76,11 @@ export class Context {
|
||||
public getFromScope (scope: unknown, paths: PropertyKey[] | string): IterableIterator<unknown> {
|
||||
return toValueSync(this._getFromScope(scope, paths))
|
||||
}
|
||||
public * _getFromScope (scope: unknown, paths: PropertyKey[] | string, strictVariables = this.strictVariables): IterableIterator<unknown> {
|
||||
public * _getFromScope (scope: unknown, paths: PropertyKey[] | string): IterableIterator<unknown> {
|
||||
if (isString(paths)) paths = paths.split('.')
|
||||
for (let i = 0; i < paths.length; i++) {
|
||||
scope = yield readProperty(scope as object, paths[i], this.ownPropertyOnly)
|
||||
if (strictVariables && isUndefined(scope)) {
|
||||
if (isNil(scope) && this.strictVariables) {
|
||||
throw new InternalUndefinedVariableError((paths as string[]).slice(0, i + 1).join!('.'))
|
||||
}
|
||||
}
|
||||
|
||||
+3
-12
@@ -14,7 +14,7 @@ export function * sort<T> (this: FilterImpl, arr: T[], property?: string): Itera
|
||||
for (const item of toArray(toValue(arr))) {
|
||||
values.push([
|
||||
item,
|
||||
property ? yield this.context._getFromScope(item, stringify(property).split('.'), false) : item
|
||||
property ? yield this.context._getFromScope(item, stringify(property).split('.')) : item
|
||||
])
|
||||
}
|
||||
return values.sort((lhs, rhs) => {
|
||||
@@ -38,20 +38,11 @@ export const size = (v: string | any[]) => (v && v.length) || 0
|
||||
export function * map (this: FilterImpl, arr: Scope[], property: string): IterableIterator<unknown> {
|
||||
const results = []
|
||||
for (const item of toArray(toValue(arr))) {
|
||||
results.push(yield this.context._getFromScope(item, stringify(property), false))
|
||||
results.push(yield this.context._getFromScope(item, stringify(property).split('.')))
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
export function * sum (this: FilterImpl, arr: Scope[], property?: string): IterableIterator<unknown> {
|
||||
let sum = 0
|
||||
for (const item of toArray(toValue(arr))) {
|
||||
const data = Number(property ? yield this.context._getFromScope(item, stringify(property), false) : item)
|
||||
sum += Number.isNaN(data) ? 0 : data
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
export function compact<T> (this: FilterImpl, arr: T[]) {
|
||||
arr = toValue(arr)
|
||||
return toArray(arr).filter(x => !isNil(toValue(x)))
|
||||
@@ -79,7 +70,7 @@ export function * where<T extends object> (this: FilterImpl, arr: T[], property:
|
||||
const values: unknown[] = []
|
||||
arr = toArray(toValue(arr))
|
||||
for (const item of arr) {
|
||||
values.push(yield this.context._getFromScope(item, stringify(property).split('.'), false))
|
||||
values.push(yield this.context._getFromScope(item, stringify(property).split('.')))
|
||||
}
|
||||
return arr.filter((_, i) => {
|
||||
if (expected === undefined) return isTruthy(values[i], this.context)
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ export function escape_once (str: string) {
|
||||
}
|
||||
|
||||
export function newline_to_br (v: string) {
|
||||
return stringify(v).replace(/\r?\n/gm, '<br />\n')
|
||||
return stringify(v).replace(/\n/g, '<br />\n')
|
||||
}
|
||||
|
||||
export function strip_html (v: string) {
|
||||
|
||||
@@ -44,7 +44,7 @@ export function remove_last (v: string, l: string) {
|
||||
const pattern = String(l)
|
||||
const index = str.lastIndexOf(pattern)
|
||||
if (index === -1) return str
|
||||
return str.substring(0, index) + str.substring(index + pattern.length)
|
||||
return str.substring(0, index) + str.substring(index + pattern.length + 1)
|
||||
}
|
||||
|
||||
export function rstrip (str: string, chars?: string) {
|
||||
@@ -74,7 +74,7 @@ export function strip (v: string, chars?: string) {
|
||||
}
|
||||
|
||||
export function strip_newlines (v: string) {
|
||||
return stringify(v).replace(/\r?\n/gm, '')
|
||||
return stringify(v).replace(/\n/g, '')
|
||||
}
|
||||
|
||||
export function capitalize (str: string) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './tokenizer'
|
||||
export * from './parser'
|
||||
export * from './parse-stream'
|
||||
export * from './parse-string-literal'
|
||||
export * from './token-kind'
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { matchOperator } from './match-operator'
|
||||
import { defaultOperators } from '..'
|
||||
import { createTrie } from '../util/operator-trie'
|
||||
|
||||
describe('parser/matchOperator()', function () {
|
||||
const trie = createTrie(defaultOperators)
|
||||
it('should match contains', () => {
|
||||
expect(matchOperator('contains', 0, trie)).toBe(8)
|
||||
})
|
||||
it('should match comparision', () => {
|
||||
expect(matchOperator('>', 0, trie)).toBe(1)
|
||||
expect(matchOperator('>=', 0, trie)).toBe(2)
|
||||
expect(matchOperator('<', 0, trie)).toBe(1)
|
||||
expect(matchOperator('<=', 0, trie)).toBe(2)
|
||||
})
|
||||
it('should match binary logic', () => {
|
||||
expect(matchOperator('and', 0, trie)).toBe(3)
|
||||
expect(matchOperator('or', 0, trie)).toBe(2)
|
||||
})
|
||||
it('should not match if word not terminate', () => {
|
||||
expect(matchOperator('true1', 0, trie)).toBe(-1)
|
||||
expect(matchOperator('containsa', 0, trie)).toBe(-1)
|
||||
})
|
||||
it('should match if word boundary found', () => {
|
||||
expect(matchOperator('>=1', 0, trie)).toBe(2)
|
||||
expect(matchOperator('contains b', 0, trie)).toBe(8)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Trie, TrieNode, IDENTIFIER, TYPES } from '../util'
|
||||
|
||||
export function matchOperator (str: string, begin: number, trie: Trie, end = str.length) {
|
||||
let node: TrieNode = trie
|
||||
let i = begin
|
||||
let info
|
||||
while (node[str[i]] && i < end) {
|
||||
node = node[str[i++]]
|
||||
if (node['end']) info = node
|
||||
}
|
||||
if (!info) return -1
|
||||
if (info['needBoundary'] && (TYPES[str.charCodeAt(i)] & IDENTIFIER)) return -1
|
||||
return i
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { parseStringLiteral } from './string'
|
||||
import { parseStringLiteral } from './parse-string-literal'
|
||||
|
||||
describe('parseStringLiteral()', function () {
|
||||
it('should parse octal escape', () => {
|
||||
@@ -48,7 +48,7 @@ export class Parser {
|
||||
}
|
||||
return new HTML(token)
|
||||
} catch (e) {
|
||||
if (LiquidError.is(e)) throw e
|
||||
if (e instanceof LiquidError) throw e
|
||||
throw new ParseError(e as Error, token)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { LiquidTagToken, HTMLToken, QuotedToken, OutputToken, TagToken, OperatorToken, RangeToken, PropertyAccessToken, NumberToken, IdentifierToken } from '../tokens'
|
||||
import { Tokenizer } from './tokenizer'
|
||||
import { defaultOperators } from '../render/operator'
|
||||
import { createTrie } from '../util/operator-trie'
|
||||
|
||||
describe('Tokenizer', function () {
|
||||
it('should read quoted', () => {
|
||||
@@ -17,31 +15,12 @@ describe('Tokenizer', function () {
|
||||
// eslint-disable-next-line deprecation/deprecation
|
||||
expect(new Tokenizer('foo bar').readWord()).toHaveProperty('content', 'foo')
|
||||
})
|
||||
it('should read integer number', () => {
|
||||
const token: NumberToken = new Tokenizer('123').readValueOrThrow() as any
|
||||
it('should read number value', () => {
|
||||
const token: NumberToken = new Tokenizer('2.33.2').readValueOrThrow() as any
|
||||
expect(token).toBeInstanceOf(NumberToken)
|
||||
expect(token.getText()).toBe('123')
|
||||
expect(token.content).toBe(123)
|
||||
})
|
||||
it('should read negative number', () => {
|
||||
const token: NumberToken = new Tokenizer('-123').readValueOrThrow() as any
|
||||
expect(token).toBeInstanceOf(NumberToken)
|
||||
expect(token.getText()).toBe('-123')
|
||||
expect(token.content).toBe(-123)
|
||||
})
|
||||
it('should read float number', () => {
|
||||
const token: NumberToken = new Tokenizer('1.23').readValueOrThrow() as any
|
||||
expect(token).toBeInstanceOf(NumberToken)
|
||||
expect(token.getText()).toBe('1.23')
|
||||
expect(token.content).toBe(1.23)
|
||||
})
|
||||
it('should treat 1.2.3 as property read', () => {
|
||||
const token: PropertyAccessToken = new Tokenizer('1.2.3').readValueOrThrow() as any
|
||||
expect(token).toBeInstanceOf(PropertyAccessToken)
|
||||
expect(token.props).toHaveLength(3)
|
||||
expect(token.props[0].getText()).toBe('1')
|
||||
expect(token.props[1].getText()).toBe('2')
|
||||
expect(token.props[2].getText()).toBe('3')
|
||||
expect(token.whole.getText()).toBe('2')
|
||||
expect(token.decimal!.getText()).toBe('33')
|
||||
expect(token.getText()).toBe('2.33')
|
||||
})
|
||||
it('should read quoted value', () => {
|
||||
const value = new Tokenizer('"foo"a').readValue()
|
||||
@@ -54,7 +33,11 @@ describe('Tokenizer', function () {
|
||||
it('should read quoted property access value', () => {
|
||||
const value = new Tokenizer('["a prop"]').readValue()
|
||||
expect(value).toBeInstanceOf(PropertyAccessToken)
|
||||
expect((value as QuotedToken).getText()).toBe('["a prop"]')
|
||||
expect((value as PropertyAccessToken).variable.getText()).toBe('"a prop"')
|
||||
})
|
||||
it('should throw for broken quoted property access', () => {
|
||||
const tokenizer = new Tokenizer('[5]')
|
||||
expect(() => tokenizer.readValueOrThrow()).toThrow()
|
||||
})
|
||||
it('should throw for incomplete quoted property access', () => {
|
||||
const tokenizer = new Tokenizer('["a prop"')
|
||||
@@ -294,10 +277,10 @@ describe('Tokenizer', function () {
|
||||
|
||||
const pa: PropertyAccessToken = token!.args[0] as any
|
||||
expect(token!.args[0]).toBeInstanceOf(PropertyAccessToken)
|
||||
expect(pa.props).toHaveLength(2)
|
||||
expect((pa.props[0] as any).content).toBe('arr')
|
||||
expect(pa.props[1]).toBeInstanceOf(NumberToken)
|
||||
expect(pa.props[1].getText()).toBe('0')
|
||||
expect((pa.variable as any).content).toBe('arr')
|
||||
expect(pa.props).toHaveLength(1)
|
||||
expect(pa.props[0]).toBeInstanceOf(NumberToken)
|
||||
expect(pa.props[0].getText()).toBe('0')
|
||||
})
|
||||
it('should read a filter with obj.foo argument', function () {
|
||||
const tokenizer = new Tokenizer('| plus: obj.foo')
|
||||
@@ -307,10 +290,10 @@ describe('Tokenizer', function () {
|
||||
|
||||
const pa: PropertyAccessToken = token!.args[0] as any
|
||||
expect(token!.args[0]).toBeInstanceOf(PropertyAccessToken)
|
||||
expect(pa.props).toHaveLength(2)
|
||||
expect((pa.props[0] as any).content).toBe('obj')
|
||||
expect(pa.props[1]).toBeInstanceOf(IdentifierToken)
|
||||
expect(pa.props[1].getText()).toBe('foo')
|
||||
expect((pa.variable as any).content).toBe('obj')
|
||||
expect(pa.props).toHaveLength(1)
|
||||
expect(pa.props[0]).toBeInstanceOf(IdentifierToken)
|
||||
expect(pa.props[0].getText()).toBe('foo')
|
||||
})
|
||||
it('should read a filter with obj["foo"] argument', function () {
|
||||
const tokenizer = new Tokenizer('| plus: obj["good luck"]')
|
||||
@@ -321,8 +304,8 @@ describe('Tokenizer', function () {
|
||||
const pa: PropertyAccessToken = token!.args[0] as any
|
||||
expect(token!.args[0]).toBeInstanceOf(PropertyAccessToken)
|
||||
expect(pa.getText()).toBe('obj["good luck"]')
|
||||
expect((pa.props[0] as any).content).toBe('obj')
|
||||
expect(pa.props[1].getText()).toBe('"good luck"')
|
||||
expect((pa.variable as any).content).toBe('obj')
|
||||
expect(pa.props[0].getText()).toBe('"good luck"')
|
||||
})
|
||||
})
|
||||
describe('#readFilters()', () => {
|
||||
@@ -358,7 +341,7 @@ describe('Tokenizer', function () {
|
||||
expect(tokens[2].args).toHaveLength(1)
|
||||
expect(tokens[2].args[0]).toBeInstanceOf(PropertyAccessToken)
|
||||
expect((tokens[2].args[0] as any).getText()).toBe('foo[a.b["c d"]]')
|
||||
expect((tokens[2].args[0] as any).props[1].getText()).toBe('a.b["c d"]')
|
||||
expect((tokens[2].args[0] as any).props[0].getText()).toBe('a.b["c d"]')
|
||||
})
|
||||
})
|
||||
describe('#readExpression()', () => {
|
||||
@@ -375,10 +358,10 @@ describe('Tokenizer', function () {
|
||||
expect(exp).toHaveLength(1)
|
||||
const pa = exp[0] as PropertyAccessToken
|
||||
expect(pa).toBeInstanceOf(PropertyAccessToken)
|
||||
expect(pa.props).toHaveLength(3)
|
||||
expect((pa.props[0] as any).content).toEqual('a')
|
||||
expect((pa.variable as any).content).toEqual('a')
|
||||
expect(pa.props).toHaveLength(2)
|
||||
|
||||
const [, p1, p2] = pa.props
|
||||
const [p1, p2] = pa.props
|
||||
expect(p1).toBeInstanceOf(IdentifierToken)
|
||||
expect(p1.getText()).toBe('')
|
||||
expect(p2).toBeInstanceOf(PropertyAccessToken)
|
||||
@@ -390,8 +373,8 @@ describe('Tokenizer', function () {
|
||||
expect(exp).toHaveLength(1)
|
||||
const pa = exp[0] as PropertyAccessToken
|
||||
expect(pa).toBeInstanceOf(PropertyAccessToken)
|
||||
expect(pa.props).toHaveLength(1)
|
||||
expect((pa.props[0] as any).content).toEqual('a')
|
||||
expect((pa.variable as any).content).toEqual('a')
|
||||
expect(pa.props).toHaveLength(0)
|
||||
})
|
||||
it('should read expression `a ==`', () => {
|
||||
const exp = [...new Tokenizer('a ==').readExpressionTokens()]
|
||||
@@ -498,30 +481,6 @@ describe('Tokenizer', function () {
|
||||
expect(rhs.getText()).toEqual('"\\""')
|
||||
})
|
||||
})
|
||||
describe('#matchTrie()', function () {
|
||||
const opTrie = createTrie(defaultOperators)
|
||||
it('should match contains', () => {
|
||||
expect(new Tokenizer('contains').matchTrie(opTrie)).toBe(8)
|
||||
})
|
||||
it('should match comparision', () => {
|
||||
expect(new Tokenizer('>').matchTrie(opTrie)).toBe(1)
|
||||
expect(new Tokenizer('>=').matchTrie(opTrie)).toBe(2)
|
||||
expect(new Tokenizer('<').matchTrie(opTrie)).toBe(1)
|
||||
expect(new Tokenizer('<=').matchTrie(opTrie)).toBe(2)
|
||||
})
|
||||
it('should match binary logic', () => {
|
||||
expect(new Tokenizer('and').matchTrie(opTrie)).toBe(3)
|
||||
expect(new Tokenizer('or').matchTrie(opTrie)).toBe(2)
|
||||
})
|
||||
it('should not match if word not terminate', () => {
|
||||
expect(new Tokenizer('true1').matchTrie(opTrie)).toBe(-1)
|
||||
expect(new Tokenizer('containsa').matchTrie(opTrie)).toBe(-1)
|
||||
})
|
||||
it('should match if word boundary found', () => {
|
||||
expect(new Tokenizer('>=1').matchTrie(opTrie)).toBe(2)
|
||||
expect(new Tokenizer('contains b').matchTrie(opTrie)).toBe(8)
|
||||
})
|
||||
})
|
||||
describe('#readLiquidTagTokens', () => {
|
||||
it('should read newline terminated tokens', () => {
|
||||
const tokenizer = new Tokenizer('echo \'hello\'')
|
||||
|
||||
+33
-72
@@ -1,17 +1,16 @@
|
||||
import { FilteredValueToken, TagToken, HTMLToken, HashToken, QuotedToken, LiquidTagToken, OutputToken, ValueToken, Token, RangeToken, FilterToken, TopLevelToken, PropertyAccessToken, OperatorToken, LiteralToken, IdentifierToken, NumberToken } from '../tokens'
|
||||
import { OperatorHandler } from '../render/operator'
|
||||
import { TrieNode, LiteralValue, Trie, createTrie, ellipsis, literalValues, TokenizationError, TYPES, QUOTE, BLANK, NUMBER, SIGN, isWord } from '../util'
|
||||
import { Trie, createTrie, ellipsis, literalValues, TokenizationError, TYPES, QUOTE, BLANK, IDENTIFIER } from '../util'
|
||||
import { Operators, Expression } from '../render'
|
||||
import { NormalizedFullOptions, defaultOptions } from '../liquid-options'
|
||||
import { FilterArg } from './filter-arg'
|
||||
import { matchOperator } from './match-operator'
|
||||
import { whiteSpaceCtrl } from './whitespace-ctrl'
|
||||
|
||||
export class Tokenizer {
|
||||
p: number
|
||||
N: number
|
||||
private rawBeginAt = -1
|
||||
private opTrie: Trie<OperatorHandler>
|
||||
private literalTrie: Trie<LiteralValue>
|
||||
private opTrie: Trie
|
||||
|
||||
constructor (
|
||||
public input: string,
|
||||
@@ -22,7 +21,6 @@ export class Tokenizer {
|
||||
this.p = range ? range[0] : 0
|
||||
this.N = range ? range[1] : input.length
|
||||
this.opTrie = createTrie(operators)
|
||||
this.literalTrie = createTrie(literalValues)
|
||||
}
|
||||
|
||||
readExpression () {
|
||||
@@ -46,22 +44,10 @@ export class Tokenizer {
|
||||
}
|
||||
readOperator (): OperatorToken | undefined {
|
||||
this.skipBlank()
|
||||
const end = this.matchTrie(this.opTrie)
|
||||
const end = matchOperator(this.input, this.p, this.opTrie)
|
||||
if (end === -1) return
|
||||
return new OperatorToken(this.input, this.p, (this.p = end), this.file)
|
||||
}
|
||||
matchTrie<T> (trie: Trie<T>) {
|
||||
let node: TrieNode<T> = trie
|
||||
let i = this.p
|
||||
let info
|
||||
while (node[this.input[i]] && i < this.N) {
|
||||
node = node[this.input[i++]]
|
||||
if (node['end']) info = node
|
||||
}
|
||||
if (!info) return -1
|
||||
if (info['needBoundary'] && isWord(this.peek(i - this.p))) return -1
|
||||
return i
|
||||
}
|
||||
readFilteredValue (): FilteredValueToken {
|
||||
const begin = this.p
|
||||
const initial = this.readExpression()
|
||||
@@ -245,7 +231,7 @@ export class Tokenizer {
|
||||
readIdentifier (): IdentifierToken {
|
||||
this.skipBlank()
|
||||
const begin = this.p
|
||||
while (!this.end() && isWord(this.peek())) ++this.p
|
||||
while (!this.end() && this.peekType() & IDENTIFIER) ++this.p
|
||||
return new IdentifierToken(this.input, begin, this.p, this.file)
|
||||
}
|
||||
|
||||
@@ -286,8 +272,8 @@ export class Tokenizer {
|
||||
return this.input.slice(this.p, this.N)
|
||||
}
|
||||
|
||||
advance (step = 1) {
|
||||
this.p += step
|
||||
advance (i = 1) {
|
||||
this.p += i
|
||||
}
|
||||
|
||||
end () {
|
||||
@@ -303,68 +289,43 @@ export class Tokenizer {
|
||||
}
|
||||
|
||||
readValue (): ValueToken | undefined {
|
||||
this.skipBlank()
|
||||
const begin = this.p
|
||||
const variable = this.readLiteral() || this.readQuoted() || this.readRange() || this.readNumber()
|
||||
const props: (ValueToken | IdentifierToken)[] = []
|
||||
const value = this.readQuoted() || this.readRange()
|
||||
if (value) return value
|
||||
|
||||
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 | IdentifierToken)[] = []
|
||||
while (true) {
|
||||
if (this.peek() === '[') {
|
||||
isNumber = false
|
||||
this.p++
|
||||
const prop = this.readValue() || new IdentifierToken(this.input, this.p, this.p, this.file)
|
||||
this.assert(this.readTo(']') !== -1, '[ not closed')
|
||||
this.readTo(']')
|
||||
props.push(prop)
|
||||
continue
|
||||
}
|
||||
if (!variable && !props.length) {
|
||||
const prop = this.readIdentifier()
|
||||
if (prop.size()) {
|
||||
props.push(prop)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if (this.peek() === '.' && this.peek(1) !== '.') { // skip range syntax
|
||||
} else if (this.peek() === '.' && this.peek(1) !== '.') { // skip range syntax
|
||||
this.p++
|
||||
const prop = this.readIdentifier()
|
||||
if (!prop.size()) break
|
||||
if (!prop.isNumber()) isNumber = false
|
||||
props.push(prop)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
if (!props.length) return variable
|
||||
return new PropertyAccessToken(variable, props, this.input, begin, this.p)
|
||||
}
|
||||
|
||||
readNumber (): NumberToken | undefined {
|
||||
this.skipBlank()
|
||||
let decimalFound = false
|
||||
let digitFound = false
|
||||
let n = 0
|
||||
if (this.peekType() & SIGN) n++
|
||||
while (this.p + n <= this.N) {
|
||||
if (this.peekType(n) & NUMBER) {
|
||||
digitFound = true
|
||||
n++
|
||||
} else if (this.peek(n) === '.' && this.peek(n + 1) !== '.') {
|
||||
if (decimalFound || !digitFound) return
|
||||
decimalFound = true
|
||||
n++
|
||||
} else break
|
||||
}
|
||||
if (digitFound && !isWord(this.peek(n))) {
|
||||
const num = new NumberToken(this.input, this.p, this.p + n, this.file)
|
||||
this.advance(n)
|
||||
return num
|
||||
if (!props.length && literalValues.hasOwnProperty(variable.content)) {
|
||||
return new LiteralToken(this.input, variable.begin, variable.end, this.file)
|
||||
}
|
||||
}
|
||||
|
||||
readLiteral (): LiteralToken | undefined {
|
||||
this.skipBlank()
|
||||
const end = this.matchTrie(this.literalTrie)
|
||||
if (end === -1) return
|
||||
const literal = new LiteralToken(this.input, this.p, end, this.file)
|
||||
this.p = end
|
||||
return literal
|
||||
if (isNumber) return new NumberToken(variable, props[0] as IdentifierToken)
|
||||
return new PropertyAccessToken(variable, props, this.p)
|
||||
}
|
||||
|
||||
readRange (): RangeToken | undefined {
|
||||
@@ -427,7 +388,7 @@ export class Tokenizer {
|
||||
}
|
||||
|
||||
peekType (n = 0) {
|
||||
return this.p + n >= this.N ? 0 : TYPES[this.input.charCodeAt(this.p + n)]
|
||||
return TYPES[this.input.charCodeAt(this.p + n)]
|
||||
}
|
||||
|
||||
peek (n = 0): string {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { Tokenizer } from '../parser'
|
||||
import { Drop } from '../drop'
|
||||
import { QuotedToken } from '../tokens'
|
||||
import { Context } from '../context'
|
||||
import { toPromise, toValueSync } from '../util'
|
||||
import { evalQuotedToken } from './expression'
|
||||
|
||||
describe('Expression', function () {
|
||||
const ctx = new Context({})
|
||||
@@ -34,9 +32,7 @@ describe('Expression', function () {
|
||||
expect(await toPromise(create('"foo"').evaluate(ctx, false))).toBe('foo')
|
||||
expect(await toPromise(create('false').evaluate(ctx, false))).toBe(false)
|
||||
})
|
||||
it('should support evalQuotedToken()', async function () {
|
||||
expect(evalQuotedToken(new QuotedToken('"foo"', 0, 5))).toBe('foo')
|
||||
})
|
||||
|
||||
it('should eval property access', async function () {
|
||||
const ctx = new Context({
|
||||
foo: { bar: 'BAR' },
|
||||
@@ -147,20 +143,6 @@ describe('Expression', function () {
|
||||
const ctx = new Context({ x: 'XXX', X: new TemplateDrop() })
|
||||
expect(await toPromise(create('x contains X').evaluate(ctx, false))).toBe(true)
|
||||
})
|
||||
it('should support Drops for "x contains "x"" when x is an array', async () => {
|
||||
class TemplateDrop extends Drop {
|
||||
valueOf () { return 'X' }
|
||||
}
|
||||
const ctx = new Context({ x: [new TemplateDrop()], X: 'X' })
|
||||
expect(await toPromise(create('x contains X').evaluate(ctx, false))).toBe(true)
|
||||
})
|
||||
it('should support Drops for "x contains "x"" when x is an array on both operands', async () => {
|
||||
class TemplateDrop extends Drop {
|
||||
valueOf () { return 'X' }
|
||||
}
|
||||
const ctx = new Context({ x: [new TemplateDrop()], X: new TemplateDrop() })
|
||||
expect(await toPromise(create('x contains X').evaluate(ctx, false))).toBe(true)
|
||||
})
|
||||
it('should support value and !=', async function () {
|
||||
const ctx = new Context({ empty: '' })
|
||||
expect(await toPromise(create('empty and empty != ""').evaluate(ctx, false))).toBe(false)
|
||||
@@ -180,10 +162,6 @@ describe('Expression', function () {
|
||||
const ctx = new Context({ obj: { foo: 'FOO' }, keys: { "what's this": 'foo' } })
|
||||
expect(await toPromise(create('obj[keys["what\'s this"]]').evaluate(ctx, false))).toBe('FOO')
|
||||
})
|
||||
it('should allow bracket quoted property access', async function () {
|
||||
const ctx = new Context({ 'foo bar': { coo: 'FOO BAR' } })
|
||||
expect(await toPromise(create('["foo bar"].coo').evaluate(ctx, false))).toBe('FOO BAR')
|
||||
})
|
||||
it('should support not', async function () {
|
||||
expect(await toPromise(create('not 1 < 2').evaluate(ctx))).toBe(false)
|
||||
})
|
||||
@@ -191,24 +169,6 @@ describe('Expression', function () {
|
||||
expect(await toPromise(create('not 1 < 2 or not 1 > 2').evaluate(ctx))).toBe(true)
|
||||
expect(await toPromise(create('not 1 < 2 and not 1 > 2').evaluate(ctx))).toBe(false)
|
||||
})
|
||||
it('should allow variable as squared sub property key', async function () {
|
||||
const ctx = new Context({ 'foo': { bar: 'BAR' }, 'key': 'bar' })
|
||||
expect(await toPromise(create('foo[key]').evaluate(ctx))).toBe('BAR')
|
||||
})
|
||||
it('should allow propertyAccessToken as squared sub property key', async function () {
|
||||
const ctx = new Context({ 'foo': { bar: 'BAR', key: 'bar' } })
|
||||
expect(await toPromise(create('foo[foo.key]').evaluate(ctx))).toBe('BAR')
|
||||
})
|
||||
it('should allow nested squared property read', async function () {
|
||||
const ctx = new Context({ 'foo': { bar: 'BAR', key: 'bar' } })
|
||||
expect(await toPromise(create('foo[foo["key"]]').evaluate(ctx))).toBe('BAR')
|
||||
})
|
||||
it('should allow string as property read variable', async function () {
|
||||
expect(await toPromise(create('"foo"[2]').evaluate(ctx))).toBe('o')
|
||||
})
|
||||
it('should allow range as property read variable', async function () {
|
||||
expect(await toPromise(create('(3..5).size').evaluate(ctx))).toBe(3)
|
||||
})
|
||||
})
|
||||
|
||||
describe('sync', function () {
|
||||
|
||||
+18
-11
@@ -1,5 +1,6 @@
|
||||
import { QuotedToken, RangeToken, OperatorToken, Token, PropertyAccessToken, OperatorType, operatorTypes } from '../tokens'
|
||||
import { isRangeToken, isPropertyAccessToken, UndefinedVariableError, range, isOperatorToken, assert } from '../util'
|
||||
import { RangeToken, OperatorToken, Token, LiteralToken, NumberToken, PropertyAccessToken, QuotedToken, OperatorType, operatorTypes } from '../tokens'
|
||||
import { isQuotedToken, isWordToken, isNumberToken, isLiteralToken, isRangeToken, isPropertyAccessToken, UndefinedVariableError, range, isOperatorToken, literalValues, assert } from '../util'
|
||||
import { parseStringLiteral } from '../parser'
|
||||
import type { Context } from '../context'
|
||||
import type { UnaryOperatorHandler } from '../render'
|
||||
|
||||
@@ -35,32 +36,38 @@ export class Expression {
|
||||
}
|
||||
|
||||
export function * evalToken (token: Token | undefined, ctx: Context, lenient = false): IterableIterator<unknown> {
|
||||
if (!token) return
|
||||
if ('content' in token) return token.content
|
||||
if (isPropertyAccessToken(token)) return yield evalPropertyAccessToken(token, ctx, lenient)
|
||||
if (isRangeToken(token)) return yield evalRangeToken(token, ctx)
|
||||
if (isLiteralToken(token)) return evalLiteralToken(token)
|
||||
if (isNumberToken(token)) return evalNumberToken(token)
|
||||
if (isWordToken(token)) return token.getText()
|
||||
if (isQuotedToken(token)) return evalQuotedToken(token)
|
||||
}
|
||||
|
||||
function * evalPropertyAccessToken (token: PropertyAccessToken, ctx: Context, lenient: boolean): IterableIterator<unknown> {
|
||||
const props: string[] = []
|
||||
const variable = yield evalToken(token.variable, ctx, lenient)
|
||||
for (const prop of token.props) {
|
||||
props.push((yield evalToken(prop, ctx, false)) as unknown as string)
|
||||
}
|
||||
try {
|
||||
if (token.variable) {
|
||||
return yield ctx._getFromScope(variable, props)
|
||||
} else {
|
||||
return yield ctx._get(props)
|
||||
}
|
||||
return yield ctx._get([token.propertyName, ...props])
|
||||
} catch (e) {
|
||||
if (lenient && (e as Error).name === 'InternalUndefinedVariableError') return null
|
||||
throw (new UndefinedVariableError(e as Error, token))
|
||||
}
|
||||
}
|
||||
|
||||
function evalNumberToken (token: NumberToken) {
|
||||
const str = token.whole.content + '.' + (token.decimal ? token.decimal.content : '')
|
||||
return Number(str)
|
||||
}
|
||||
|
||||
export function evalQuotedToken (token: QuotedToken) {
|
||||
return token.content
|
||||
return parseStringLiteral(token.getText())
|
||||
}
|
||||
|
||||
function evalLiteralToken (token: LiteralToken) {
|
||||
return literalValues[token.literal]
|
||||
}
|
||||
|
||||
function * evalRangeToken (token: RangeToken, ctx: Context) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { isComparable } from '../drop/comparable'
|
||||
import { Context } from '../context'
|
||||
import { toValue } from '../util'
|
||||
import { isFunction, toValue } from '../util'
|
||||
import { isFalsy, isTruthy } from '../render/boolean'
|
||||
import { isArray, isFunction } from '../util/underscore'
|
||||
import { isArray } from '../util/underscore'
|
||||
|
||||
export type UnaryOperatorHandler = (operand: any, ctx: Context) => boolean;
|
||||
export type BinaryOperatorHandler = (lhs: any, rhs: any, ctx: Context) => boolean;
|
||||
@@ -34,9 +34,8 @@ export const defaultOperators: Operators = {
|
||||
},
|
||||
'contains': (l: any, r: any) => {
|
||||
l = toValue(l)
|
||||
if (isArray(l)) return l.some((i) => equal(i, r))
|
||||
if (isFunction(l?.indexOf)) return l.indexOf(toValue(r)) > -1
|
||||
return false
|
||||
r = toValue(r)
|
||||
return l && isFunction(l.indexOf) ? l.indexOf(r) > -1 : false
|
||||
},
|
||||
'not': (v: any, ctx: Context) => isFalsy(toValue(v), ctx),
|
||||
'and': (l: any, r: any, ctx: Context) => isTruthy(toValue(l), ctx) && isTruthy(toValue(r), ctx),
|
||||
|
||||
+3
-20
@@ -10,40 +10,23 @@ export default class extends Tag {
|
||||
this.elseTemplates = []
|
||||
|
||||
let p: Template[] = []
|
||||
let elseCount = 0
|
||||
const stream: ParseStream = this.liquid.parser.parseStream(remainTokens)
|
||||
.on('tag:when', (token: TagToken) => {
|
||||
if (elseCount > 0) {
|
||||
return
|
||||
}
|
||||
|
||||
p = []
|
||||
|
||||
const values: ValueToken[] = []
|
||||
while (!token.tokenizer.end()) {
|
||||
values.push(token.tokenizer.readValueOrThrow())
|
||||
token.tokenizer.skipBlank()
|
||||
if (token.tokenizer.peek() === ',') {
|
||||
token.tokenizer.readTo(',')
|
||||
} else {
|
||||
token.tokenizer.readTo('or')
|
||||
}
|
||||
token.tokenizer.readTo(',')
|
||||
}
|
||||
this.branches.push({
|
||||
values,
|
||||
templates: p
|
||||
})
|
||||
})
|
||||
.on('tag:else', () => {
|
||||
elseCount++
|
||||
p = this.elseTemplates
|
||||
})
|
||||
.on('tag:else', () => (p = this.elseTemplates))
|
||||
.on('tag:endcase', () => stream.stop())
|
||||
.on('template', (tpl: Template) => {
|
||||
if (p !== this.elseTemplates || elseCount === 1) {
|
||||
p.push(tpl)
|
||||
}
|
||||
})
|
||||
.on('template', (tpl: Template) => p.push(tpl))
|
||||
.on('end', () => {
|
||||
throw new Error(`tag ${tagToken.getText()} not closed`)
|
||||
})
|
||||
|
||||
+7
-21
@@ -6,33 +6,19 @@ export default class extends Tag {
|
||||
|
||||
constructor (tagToken: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
|
||||
super(tagToken, remainTokens, liquid)
|
||||
let p: Template[] = []
|
||||
let elseCount = 0
|
||||
let p
|
||||
liquid.parser.parseStream(remainTokens)
|
||||
.on('start', () => this.branches.push({
|
||||
value: new Value(tagToken.args, this.liquid),
|
||||
templates: (p = [])
|
||||
}))
|
||||
.on('tag:elsif', (token: TagToken) => {
|
||||
if (elseCount > 0) {
|
||||
p = []
|
||||
return
|
||||
}
|
||||
this.branches.push({
|
||||
value: new Value(token.args, this.liquid),
|
||||
templates: (p = [])
|
||||
})
|
||||
})
|
||||
.on('tag:else', () => {
|
||||
elseCount++
|
||||
p = this.elseTemplates
|
||||
})
|
||||
.on('tag:elsif', (token: TagToken) => this.branches.push({
|
||||
value: new Value(token.args, this.liquid),
|
||||
templates: (p = [])
|
||||
}))
|
||||
.on('tag:else', () => (p = this.elseTemplates))
|
||||
.on('tag:endif', function () { this.stop() })
|
||||
.on('template', (tpl: Template) => {
|
||||
if (p !== this.elseTemplates || elseCount === 1) {
|
||||
p.push(tpl)
|
||||
}
|
||||
})
|
||||
.on('template', (tpl: Template) => p.push(tpl))
|
||||
.on('end', () => { throw new Error(`tag ${tagToken.getText()} not closed`) })
|
||||
.start()
|
||||
}
|
||||
|
||||
+8
-22
@@ -5,35 +5,21 @@ export default class extends Tag {
|
||||
elseTemplates: Template[] = []
|
||||
constructor (tagToken: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
|
||||
super(tagToken, remainTokens, liquid)
|
||||
let p: Template[] = []
|
||||
let elseCount = 0
|
||||
let p
|
||||
this.liquid.parser.parseStream(remainTokens)
|
||||
.on('start', () => this.branches.push({
|
||||
value: new Value(tagToken.args, this.liquid),
|
||||
test: isFalsy,
|
||||
templates: (p = [])
|
||||
}))
|
||||
.on('tag:elsif', (token: TagToken) => {
|
||||
if (elseCount > 0) {
|
||||
p = []
|
||||
return
|
||||
}
|
||||
this.branches.push({
|
||||
value: new Value(token.args, this.liquid),
|
||||
test: isTruthy,
|
||||
templates: (p = [])
|
||||
})
|
||||
})
|
||||
.on('tag:else', () => {
|
||||
elseCount++
|
||||
p = this.elseTemplates
|
||||
})
|
||||
.on('tag:elsif', (token: TagToken) => this.branches.push({
|
||||
value: new Value(token.args, this.liquid),
|
||||
test: isTruthy,
|
||||
templates: (p = [])
|
||||
}))
|
||||
.on('tag:else', () => (p = this.elseTemplates))
|
||||
.on('tag:endunless', function () { this.stop() })
|
||||
.on('template', (tpl: Template) => {
|
||||
if (p !== this.elseTemplates || elseCount === 1) {
|
||||
p.push(tpl)
|
||||
}
|
||||
})
|
||||
.on('template', (tpl: Template) => p.push(tpl))
|
||||
.on('end', () => { throw new Error(`tag ${tagToken.getText()} not closed`) })
|
||||
.start()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Context } from '../context'
|
||||
import { toPromise } from '../util'
|
||||
import { NumberToken, QuotedToken } from '../tokens'
|
||||
import { IdentifierToken, NumberToken, QuotedToken } from '../tokens'
|
||||
import { Filter } from './filter'
|
||||
|
||||
describe('filter', function () {
|
||||
@@ -13,7 +13,7 @@ describe('filter', function () {
|
||||
|
||||
it('should call filter impl with correct arguments', async function () {
|
||||
const spy = jest.fn()
|
||||
const thirty = new NumberToken('30', 0, 2, undefined)
|
||||
const thirty = new NumberToken(new IdentifierToken('30', 0, 2), undefined)
|
||||
const filter = new Filter('foo', spy, [thirty], liquid)
|
||||
await toPromise(filter.render('foo', ctx))
|
||||
expect(spy).toHaveBeenCalledWith('foo', 30)
|
||||
@@ -23,7 +23,7 @@ describe('filter', function () {
|
||||
const val = yield this.context._get([valStr])
|
||||
return `${this.liquid.testVersion}: ${val + diff}`
|
||||
})
|
||||
const ten = new NumberToken('10', 0, 2, undefined)
|
||||
const ten = new NumberToken(new IdentifierToken('10', 0, 2), undefined)
|
||||
const filter = new Filter('add', spy, [ten], liquid)
|
||||
const val = await toPromise(filter.render('thirty', ctx))
|
||||
expect(val).toEqual('1.0: 40')
|
||||
@@ -31,19 +31,14 @@ describe('filter', function () {
|
||||
it('should render a simple filter', async function () {
|
||||
expect(await toPromise(new Filter('upcase', (x: string) => x.toUpperCase(), [], liquid).render('foo', ctx))).toBe('FOO')
|
||||
})
|
||||
it('should reject promise when filter throws', async function () {
|
||||
const filter = new Filter('foo', function * () { throw new Error('intended') }, [], liquid)
|
||||
expect(toPromise(filter.render('foo', ctx))).rejects.toMatchObject({
|
||||
message: 'intended'
|
||||
})
|
||||
})
|
||||
|
||||
it('should render filters with argument', async function () {
|
||||
const two = new NumberToken('2', 0, 1, undefined)
|
||||
const two = new NumberToken(new IdentifierToken('2', 0, 1), undefined)
|
||||
expect(await toPromise(new Filter('add', (a: number, b: number) => a + b, [two], liquid).render(3, ctx))).toBe(5)
|
||||
})
|
||||
|
||||
it('should render filters with multiple arguments', async function () {
|
||||
const two = new NumberToken('2', 0, 1, undefined)
|
||||
const two = new NumberToken(new IdentifierToken('2', 0, 1), undefined)
|
||||
const c = new QuotedToken('"c"', 0, 3)
|
||||
expect(await toPromise(new Filter('add', (a: number, b: number, c: number) => a + b + c, [two, c], liquid).render(3, ctx))).toBe('5c')
|
||||
})
|
||||
@@ -54,7 +49,7 @@ describe('filter', function () {
|
||||
})
|
||||
|
||||
it('should support key value pairs', async function () {
|
||||
const two = new NumberToken('2', 0, 1, undefined)
|
||||
const two = new NumberToken(new IdentifierToken('2', 0, 1), undefined)
|
||||
expect(await toPromise(new Filter('add', (a: number, b: number[]) => b[0] + ':' + (a + b[1]), [['num', two]], liquid).render(3, ctx))).toBe('num:5')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -27,6 +27,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.handler.apply({ context, liquid: this.liquid }, [value, ...argv])
|
||||
return this.handler.apply({ context, liquid: this.liquid }, [value, ...argv])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { Token } from './token'
|
||||
import { TokenKind } from '../parser'
|
||||
import { literalValues, LiteralValue } from '../util'
|
||||
|
||||
export class LiteralToken extends Token {
|
||||
public content: LiteralValue
|
||||
public literal: string
|
||||
public constructor (
|
||||
public input: string,
|
||||
@@ -13,6 +11,5 @@ export class LiteralToken extends Token {
|
||||
) {
|
||||
super(TokenKind.Literal, input, begin, end, file)
|
||||
this.literal = this.getText()
|
||||
this.content = literalValues[this.literal]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import { Token } from './token'
|
||||
import { IdentifierToken } from './identifier-token'
|
||||
import { TokenKind } from '../parser'
|
||||
|
||||
export class NumberToken extends Token {
|
||||
public content: number
|
||||
constructor (
|
||||
public input: string,
|
||||
public begin: number,
|
||||
public end: number,
|
||||
public file?: string
|
||||
public whole: IdentifierToken,
|
||||
public decimal?: IdentifierToken
|
||||
) {
|
||||
super(TokenKind.Number, input, begin, end, file)
|
||||
this.content = Number(this.getText())
|
||||
super(TokenKind.Number, whole.input, whole.begin, decimal ? decimal.end : whole.end, whole.file)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { QuotedToken, PropertyAccessToken, IdentifierToken } from '.'
|
||||
|
||||
describe('PropertyAccessToken', function () {
|
||||
describe('#propertyName', function () {
|
||||
it('should return correct value for IdentifierToken', function () {
|
||||
const token = new PropertyAccessToken(new IdentifierToken('foo', 0, 3), [], 3)
|
||||
expect(token.propertyName).toBe('foo')
|
||||
})
|
||||
it('should return correct value for QuotedToken', function () {
|
||||
const token = new PropertyAccessToken(new QuotedToken('"foo bar"', 0, 9), [], 9)
|
||||
expect(token.propertyName).toBe('foo bar')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,21 +1,18 @@
|
||||
import { Token } from './token'
|
||||
import { LiteralToken } from './literal-token'
|
||||
import { ValueToken } from './value-token'
|
||||
import { IdentifierToken } from './identifier-token'
|
||||
import { NumberToken } from './number-token'
|
||||
import { RangeToken } from './range-token'
|
||||
import { QuotedToken } from './quoted-token'
|
||||
import { TokenKind } from '../parser'
|
||||
import { TokenKind, parseStringLiteral } from '../parser'
|
||||
|
||||
export class PropertyAccessToken extends Token {
|
||||
public propertyName: string
|
||||
constructor (
|
||||
public variable: QuotedToken | RangeToken | LiteralToken | NumberToken | undefined,
|
||||
public props: (ValueToken | IdentifierToken)[],
|
||||
input: string,
|
||||
begin: number,
|
||||
end: number,
|
||||
file?: string
|
||||
public variable: IdentifierToken | QuotedToken,
|
||||
public props: (IdentifierToken | QuotedToken | PropertyAccessToken)[],
|
||||
end: number
|
||||
) {
|
||||
super(TokenKind.PropertyAccess, input, begin, end, file)
|
||||
super(TokenKind.PropertyAccess, variable.input, variable.begin, end, variable.file)
|
||||
this.propertyName = this.variable instanceof IdentifierToken
|
||||
? this.variable.getText()
|
||||
: parseStringLiteral(this.variable.getText())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { Token } from './token'
|
||||
import { TokenKind } from '../parser'
|
||||
import { parseStringLiteral } from '../render/string'
|
||||
|
||||
export class QuotedToken extends Token {
|
||||
public readonly content: string
|
||||
constructor (
|
||||
public input: string,
|
||||
public begin: number,
|
||||
@@ -11,6 +9,5 @@ export class QuotedToken extends Token {
|
||||
public file?: string
|
||||
) {
|
||||
super(TokenKind.Quoted, input, begin, end, file)
|
||||
this.content = parseStringLiteral(this.getText())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { RangeToken } from './range-token'
|
||||
import { LiteralToken } from './literal-token'
|
||||
import { NumberToken } from './number-token'
|
||||
import { QuotedToken } from './quoted-token'
|
||||
import { PropertyAccessToken } from './property-access-token'
|
||||
|
||||
export type ValueToken = RangeToken | LiteralToken | QuotedToken | PropertyAccessToken | NumberToken
|
||||
export type ValueToken = RangeToken | LiteralToken | QuotedToken | PropertyAccessToken
|
||||
|
||||
@@ -3,18 +3,11 @@
|
||||
// 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 WORD = 1
|
||||
export const IDENTIFIER = 1
|
||||
export const OPERATOR = 2
|
||||
export const BLANK = 4
|
||||
export const QUOTE = 8
|
||||
export const INLINE_BLANK = 16
|
||||
export const NUMBER = 32
|
||||
export const SIGN = 64
|
||||
export const PUNCTUATION = 128
|
||||
|
||||
export function isWord (char: string): boolean {
|
||||
const code = char.charCodeAt(0)
|
||||
return code >= 128 ? !TYPES[code] : !!(TYPES[code] & WORD)
|
||||
}
|
||||
TYPES[160] = TYPES[5760] = TYPES[6158] = TYPES[8192] = TYPES[8193] = TYPES[8194] = TYPES[8195] = TYPES[8196] = TYPES[8197] = TYPES[8198] = TYPES[8199] = TYPES[8200] = TYPES[8201] = TYPES[8202] = TYPES[8232] = TYPES[8233] = TYPES[8239] = TYPES[8287] = TYPES[12288] = BLANK
|
||||
TYPES[8220] = TYPES[8221] = PUNCTUATION
|
||||
|
||||
@@ -2,11 +2,6 @@ import * as _ from './underscore'
|
||||
import { Token } from '../tokens/token'
|
||||
import { Template } from '../template/template'
|
||||
|
||||
/**
|
||||
* targeting ES5, extends Error won't create a proper prototype chain, need a trait to kee track of classes
|
||||
*/
|
||||
const TRAIT = '__liquidClass__'
|
||||
|
||||
export abstract class LiquidError extends Error {
|
||||
private token!: Token
|
||||
public context = ''
|
||||
@@ -19,7 +14,6 @@ export abstract class LiquidError extends Error {
|
||||
super(typeof err === 'string' ? err : err.message)
|
||||
if (typeof err !== 'string') Object.defineProperty(this, 'originalError', { value: err, enumerable: false })
|
||||
Object.defineProperty(this, 'token', { value: token, enumerable: false })
|
||||
Object.defineProperty(this, TRAIT, { value: 'LiquidError', enumerable: false })
|
||||
}
|
||||
protected update () {
|
||||
Object.defineProperty(this, 'context', { value: mkContext(this.token), enumerable: false })
|
||||
@@ -28,9 +22,6 @@ export abstract class LiquidError extends Error {
|
||||
'\n' + this.stack
|
||||
if (this.originalError) this.stack += '\nFrom ' + this.originalError.stack
|
||||
}
|
||||
static is (obj: unknown): obj is LiquidError {
|
||||
return obj?.[TRAIT] === 'LiquidError'
|
||||
}
|
||||
}
|
||||
|
||||
export class TokenizationError extends LiquidError {
|
||||
|
||||
@@ -9,6 +9,3 @@ export const literalValues = {
|
||||
'empty': new EmptyDrop(),
|
||||
'blank': new BlankDrop()
|
||||
}
|
||||
|
||||
export type LiteralKey = keyof typeof literalValues
|
||||
export type LiteralValue = typeof literalValues[LiteralKey]
|
||||
|
||||
+13
-16
@@ -1,38 +1,35 @@
|
||||
import { isWord } from '../util/character'
|
||||
import { Operators, OperatorHandler } from '../render/operator'
|
||||
import { IDENTIFIER, TYPES } from '../util/character'
|
||||
|
||||
interface TrieInput<T> {
|
||||
[key: string]: T
|
||||
}
|
||||
|
||||
interface TrieLeafNode<T> {
|
||||
data: T;
|
||||
interface TrieLeafNode {
|
||||
handler: OperatorHandler;
|
||||
end: true;
|
||||
needBoundary?: true;
|
||||
}
|
||||
|
||||
export interface Trie<T> {
|
||||
[key: string]: Trie<T> | TrieLeafNode<T>;
|
||||
export interface Trie {
|
||||
[key: string]: Trie | TrieLeafNode;
|
||||
}
|
||||
|
||||
export type TrieNode<T> = Trie<T> | TrieLeafNode<T>
|
||||
export type TrieNode = Trie | TrieLeafNode
|
||||
|
||||
export function createTrie<T = any> (input: TrieInput<T>): Trie<T> {
|
||||
const trie: Trie<T> = {}
|
||||
for (const [name, data] of Object.entries(input)) {
|
||||
let node: Trie<T> | TrieLeafNode<T> = trie
|
||||
export function createTrie (operators: Operators): Trie {
|
||||
const trie: Trie = {}
|
||||
for (const [name, handler] of Object.entries(operators)) {
|
||||
let node: Trie | TrieLeafNode = trie
|
||||
|
||||
for (let i = 0; i < name.length; i++) {
|
||||
const c = name[i]
|
||||
node[c] = node[c] || {}
|
||||
|
||||
if (i === name.length - 1 && isWord(name[i])) {
|
||||
if (i === name.length - 1 && (TYPES[name.charCodeAt(i)] & IDENTIFIER)) {
|
||||
node[c].needBoundary = true
|
||||
}
|
||||
|
||||
node = node[c]
|
||||
}
|
||||
|
||||
node.data = data
|
||||
node.handler = handler
|
||||
node.end = true
|
||||
}
|
||||
return trie
|
||||
|
||||
@@ -63,10 +63,6 @@ export function isNil (value: any): boolean {
|
||||
return value == null
|
||||
}
|
||||
|
||||
export function isUndefined (value: any): boolean {
|
||||
return value === undefined
|
||||
}
|
||||
|
||||
export function isArray (value: any): value is any[] {
|
||||
// be compatible with IE 8
|
||||
return toString.call(value) === '[object Array]'
|
||||
|
||||
@@ -7,18 +7,4 @@ describe('browser', function () {
|
||||
message: 'output "{{huh" not closed, line:1, col:1'
|
||||
})
|
||||
})
|
||||
it('should throw tokenization error for invalid filter syntax', async () => {
|
||||
const engine = new LiquidUMD()
|
||||
const message = 'expected filter name, line:1, col:10'
|
||||
const stack = [
|
||||
'>> 1| {{ foo | ^ }}',
|
||||
' ^',
|
||||
`TokenizationError: ${message}`
|
||||
].join('\n')
|
||||
await expect(engine.parseAndRender('{{ foo | ^ }}')).rejects.toMatchObject({
|
||||
message,
|
||||
stack: expect.stringContaining(stack),
|
||||
name: 'TokenizationError'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -454,49 +454,4 @@ describe('Issues', function () {
|
||||
const result = engine.parseAndRenderSync(template, { product })
|
||||
expect(result).toEqual('This is a love potion!')
|
||||
})
|
||||
it('#643 Error When Accessing Subproperty of Bracketed Reference', () => {
|
||||
const engine = new Liquid()
|
||||
const tpl = '{{ ["Key String with Spaces"].subpropertyKey }}'
|
||||
const ctx = {
|
||||
'Key String with Spaces': {
|
||||
subpropertyKey: 'FOO'
|
||||
}
|
||||
}
|
||||
expect(engine.parseAndRenderSync(tpl, ctx)).toEqual('FOO')
|
||||
})
|
||||
it('#655 Error in the tokenization process due to an invalid value expression', () => {
|
||||
const engine = new Liquid()
|
||||
const result = engine.parseAndRenderSync('{{ÜLKE}}', { ÜLKE: 'Türkiye' })
|
||||
expect(result).toEqual('Türkiye')
|
||||
})
|
||||
it('#670 Should not render anything after an else branch', () => {
|
||||
const engine = new Liquid()
|
||||
const result = engine.parseAndRenderSync('{% assign value = "this" %}' +
|
||||
'{% if false %}don\'t show' +
|
||||
'{% else %}show {{ value }}' +
|
||||
'{% else %}don\'t show{% endif %}', {})
|
||||
expect(result).toEqual('show this')
|
||||
})
|
||||
it('#672 Should not render an elseif after an else branch', () => {
|
||||
const engine = new Liquid()
|
||||
const result = engine.parseAndRenderSync('{% if false %}don\'t show' +
|
||||
'{% else %}show' +
|
||||
'{% elsif true %}don\'t show' +
|
||||
'{% endif %}', {})
|
||||
expect(result).toEqual('show')
|
||||
})
|
||||
it('#675 10.10.1 Operator: contains regression', () => {
|
||||
const engine = new Liquid()
|
||||
class StrictStringForLiquid {
|
||||
constructor (private value: string) {}
|
||||
indexOf (other: unknown) {
|
||||
return this.value.indexOf(String(other))
|
||||
}
|
||||
}
|
||||
const result = engine.parseAndRenderSync('{{ str contains sub }}', {
|
||||
str: new StrictStringForLiquid('string'),
|
||||
sub: 'str'
|
||||
})
|
||||
expect(result).toEqual('true')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -58,14 +58,6 @@ describe('filters/array', function () {
|
||||
const post = { category: 'foo' }
|
||||
return test('{{post | map: "category"}}', { post }, 'foo')
|
||||
})
|
||||
it('should allow nil results in strictVariables mode', function () {
|
||||
const engine = new Liquid({ strictVariables: true })
|
||||
const ctx = {
|
||||
posts: [{ category: 'foo' }, { title: 'bar' }]
|
||||
}
|
||||
const result = engine.parseAndRenderSync('{{posts | map: "category" | json}}', ctx)
|
||||
expect(result).toEqual('["foo",null]')
|
||||
})
|
||||
it('should support nested property', function () {
|
||||
const tpl = '{{ arr | map: "name.first" | join }}'
|
||||
const a = { name: { first: 'Alice' } }
|
||||
@@ -74,44 +66,6 @@ describe('filters/array', function () {
|
||||
return test(tpl, { arr: [a, b, c] }, 'Alice Bob Carol')
|
||||
})
|
||||
})
|
||||
describe('sum', () => {
|
||||
it('should support sum with no args', function () {
|
||||
const ages = [21, null, -4, '4.5', 13.25, undefined, 0]
|
||||
return test('{{ages | sum}}', { ages }, '34.75')
|
||||
})
|
||||
it('should support sum with property', function () {
|
||||
const ages = [21, null, -4, '4.5', 13.25, undefined, 0].map(x => ({ age: x }))
|
||||
return test('{{ages | sum: "age"}}', { ages }, '34.75')
|
||||
})
|
||||
it('should support sum with nested property', function () {
|
||||
const ages = [21, null, -4, '4.5', 13.25, undefined, 0].map(x => ({ age: { first: x } }))
|
||||
return test('{{ages | sum: "age.first"}}', { ages }, '34.75')
|
||||
})
|
||||
it('should support non-array input', function () {
|
||||
const age = 21.5
|
||||
return test('{{age | sum}}', { age }, '21.5')
|
||||
})
|
||||
it('should coerce missing property to zero', function () {
|
||||
const ages = [{ qty: 1 }, { qty: 2, cnt: 3 }, { cnt: 4 }]
|
||||
return test('{{ages | sum}} {{ages | sum: "cnt"}} {{ages | sum: "other"}}', { ages }, '0 7 0')
|
||||
})
|
||||
it('should coerce indexable non-map values to zero', function () {
|
||||
const input = [1, 'foo', { quantity: 3 }]
|
||||
return test('{{input | sum}}', { input }, '1')
|
||||
})
|
||||
it('should coerce unindexable values to zero', function () {
|
||||
const input = [1, null, { quantity: 2 }]
|
||||
return test('{{input | sum}}', { input }, '1')
|
||||
})
|
||||
it('should coerce true to 1', function () {
|
||||
const input = [1, true, null, { quantity: 2 }]
|
||||
return test('{{input | sum}}', { input }, '2')
|
||||
})
|
||||
it('should not support nested arrays', function () {
|
||||
const ages = [1, [2, [3, 4]]]
|
||||
return test('{{ages | sum}}', { ages }, '1')
|
||||
})
|
||||
})
|
||||
describe('compact', () => {
|
||||
it('should compact array', function () {
|
||||
const posts = [{ category: 'foo' }, { category: 'bar' }, { foo: 'bar' }]
|
||||
|
||||
@@ -30,7 +30,7 @@ describe('filters/html', function () {
|
||||
it('should support string_with_newlines', function () {
|
||||
const src = '{% capture string_with_newlines %}\n' +
|
||||
'Hello\n' +
|
||||
'there\r\n' +
|
||||
'there\n' +
|
||||
'{% endcapture %}' +
|
||||
'{{ string_with_newlines | newline_to_br }}'
|
||||
const dst = '<br />\n' +
|
||||
|
||||
@@ -137,12 +137,6 @@ describe('filters/string', function () {
|
||||
'{{ string_with_newlines | strip_newlines }}',
|
||||
'Hellothere')
|
||||
})
|
||||
it('should support strip_newlines on Windows newlines ', function () {
|
||||
return test('{% capture string_with_newlines %}\n' +
|
||||
'Hello\r\nthere\n{% endcapture %}' +
|
||||
'{{ string_with_newlines | strip_newlines }}',
|
||||
'Hellothere')
|
||||
})
|
||||
describe('truncate', function () {
|
||||
it('should truncate when string too long', function () {
|
||||
return test('{{ "Ground control to Major Tom." | truncate: 20 }}',
|
||||
@@ -209,10 +203,6 @@ describe('filters/string', function () {
|
||||
return test('{{ "I strained to see the train through the rain" | remove_last: "rain" }}',
|
||||
'I strained to see the train through the ')
|
||||
})
|
||||
it('should remove the last occurrence of substring in the middle of a string', function () {
|
||||
return test('{{ "I strained to see the train through the rain and fog" | remove_last: "rain" }}',
|
||||
'I strained to see the train through the and fog')
|
||||
})
|
||||
it('should handle substring not found', function () {
|
||||
return test('{{ "I strained to see the train through the rain" | remove_last: "no such thing" }}',
|
||||
'I strained to see the train through the rain')
|
||||
|
||||
@@ -75,48 +75,4 @@ describe('tags/case', function () {
|
||||
const html = await liquid.parseAndRender(src)
|
||||
return expect(html).toBe('firstsecond')
|
||||
})
|
||||
it('should support case with multiple values separated by or', async function () {
|
||||
const src = '{% case 3 %}' +
|
||||
'{% when 1 or 2 or 3 %}1 or 2 or 3' +
|
||||
'{% else %}not 1 or 2 or 3' +
|
||||
'{%endcase%}'
|
||||
const html = await liquid.parseAndRender(src)
|
||||
return expect(html).toBe('1 or 2 or 3')
|
||||
})
|
||||
it('should support case with multiple strings separated by or', async function () {
|
||||
const src = '{% case "or" %}' +
|
||||
'{% when "and" or "or" %}and or or' +
|
||||
'{% else %}not and or or' +
|
||||
'{%endcase%}'
|
||||
const html = await liquid.parseAndRender(src)
|
||||
return expect(html).toBe('and or or')
|
||||
})
|
||||
it('should not render anything after an else branch', async function () {
|
||||
const html = await liquid.parseAndRenderSync('{% assign value = "this" %}' +
|
||||
'{% case true %}' +
|
||||
'{% when false %}don\'t show' +
|
||||
'{% else %}show {{ value }}' +
|
||||
'{% else %}don\'t show' +
|
||||
'{% endcase %}', {})
|
||||
expect(html).toEqual('show this')
|
||||
})
|
||||
it('should not render anything after an else branch even when first else branch is empty', async function () {
|
||||
const html = await liquid.parseAndRenderSync('{% case true %}' +
|
||||
'{% when false %}don\'t show' +
|
||||
'{% else %}' +
|
||||
'{% else %}don\'t show' +
|
||||
'{% endcase %}', {})
|
||||
expect(html).toEqual('')
|
||||
})
|
||||
it('should not render anything after an else branch even when there are \'when\' conditions', () => {
|
||||
const engine = new Liquid()
|
||||
const result = engine.parseAndRenderSync('{% assign value = "this" %}' +
|
||||
'{% case true -%}' +
|
||||
'{% when false -%}don\'t show' +
|
||||
'{% else %}show {{ value }}' +
|
||||
'{% else %}don\'t show' +
|
||||
'{%- when true -%}don\'t show' +
|
||||
'{%- endcase %}', {})
|
||||
expect(result).toEqual('show this')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -59,12 +59,6 @@ describe('tags/for', function () {
|
||||
const html = await liquid.parseAndRender(src, scope)
|
||||
return expect(html).toBe('str-"string"-string')
|
||||
})
|
||||
it('should not report undefined variable on null value', async function () {
|
||||
const engine = new Liquid({ strictVariables: true })
|
||||
const src = '{% assign hello = "hello,world" | split: "," | concat: null %}{% for i in hello %}{{ i }},{% endfor %}'
|
||||
const html = await engine.parseAndRender(src, scope)
|
||||
return expect(html).toBe('hello,world,,')
|
||||
})
|
||||
describe('illegal', function () {
|
||||
it('should reject when for not closed', function () {
|
||||
const src = '{%for c in alpha%}{{c}}'
|
||||
|
||||
@@ -143,12 +143,4 @@ describe('tags/if', function () {
|
||||
const html = await liquid.parseAndRender(src, scope)
|
||||
return expect(html).toBe('success')
|
||||
})
|
||||
it('should not render anything after an else branch even when first else branch is empty', () => {
|
||||
const engine = new Liquid()
|
||||
const result = engine.parseAndRenderSync('{% if false %}don\'t show' +
|
||||
'{% else %}' +
|
||||
'{% else %}don\'t show' +
|
||||
'%{% endif %}', {})
|
||||
expect(result).toEqual('')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -47,29 +47,6 @@ describe('tags/unless', function () {
|
||||
Inside wonderland
|
||||
After wonderland`)
|
||||
})
|
||||
it('should not render anything after an else branch', async function () {
|
||||
const html = await liquid.parseAndRenderSync('{% assign value = "this" %}' +
|
||||
'{% unless true %}don\'t show' +
|
||||
'{% else %}show {{ value }}' +
|
||||
'{% else %}don\'t show' +
|
||||
'{% endunless %}', {})
|
||||
expect(html).toEqual('show this')
|
||||
})
|
||||
it('should not render anything after an else branch even when first else branch is empty', async function () {
|
||||
const html = await liquid.parseAndRenderSync('{% unless true %}don\'t show' +
|
||||
'{% else %}' +
|
||||
'{% else %}don\'t show' +
|
||||
'{% endunless %}', {})
|
||||
expect(html).toEqual('')
|
||||
})
|
||||
it('should not render an elseif after an else branch', () => {
|
||||
const engine = new Liquid()
|
||||
const result = engine.parseAndRenderSync('{% unless true %}don\'t show' +
|
||||
'{% else %}show' +
|
||||
'{% elsif true %}don\'t show' +
|
||||
'{% endunless %}', {})
|
||||
expect(result).toEqual('show')
|
||||
})
|
||||
|
||||
describe('sync support', function () {
|
||||
it('should render else when predicate yields true', function () {
|
||||
|
||||
Reference in New Issue
Block a user