Compare commits

..
67 changed files with 346 additions and 1209 deletions
+29 -55
View File
@@ -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,
+16
View File
@@ -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 }}
-65
View File
@@ -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)
+14 -33
View File
@@ -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
View File
@@ -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/"/\&quot;/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
View File
@@ -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
View File
@@ -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
View File
@@ -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
-2
View File
@@ -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
-22
View File
@@ -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/
-201
View File
@@ -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
+1 -1
View File
@@ -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 %}
+1 -1
View File
@@ -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
-201
View File
@@ -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
+4 -2
View File
@@ -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&#39;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
-1
View File
@@ -44,7 +44,6 @@ sidebar:
access_scope_in_filters: 过滤器里访问上下文
parse_parameters: 参数解析
render_tag_content: 渲染标签内容
drops: Liquid Drop
sync_and_async: 同步和异步
whitespace: 换行和缩进
plugins: 插件
+11 -4
View File
@@ -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>
-7
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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%)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "liquidjs",
"version": "10.10.2",
"version": "10.8.4",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
+2 -4
View File
@@ -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
View File
@@ -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'
}
}
})
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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) {
+2 -2
View File
@@ -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
View File
@@ -1,4 +1,5 @@
export * from './tokenizer'
export * from './parser'
export * from './parse-stream'
export * from './parse-string-literal'
export * from './token-kind'
+28
View File
@@ -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)
})
})
+14
View File
@@ -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', () => {
+1 -1
View File
@@ -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)
}
}
+26 -67
View File
@@ -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
View File
@@ -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 -41
View File
@@ -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
View File
@@ -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) {
+4 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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()
}
+7 -12
View File
@@ -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')
})
})
+1 -1
View File
@@ -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])
}
}
-3
View File
@@ -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]
}
}
+4 -7
View File
@@ -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)
}
}
+14
View 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')
})
})
})
+9 -12
View File
@@ -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())
}
}
-3
View File
@@ -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 -2
View File
@@ -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
+1 -8
View File
@@ -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
-9
View File
@@ -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 {
-3
View File
@@ -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
View File
@@ -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
-4
View File
@@ -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]'
-14
View File
@@ -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'
})
})
})
-45
View File
@@ -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')
})
})
-46
View File
@@ -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' }]
+1 -1
View File
@@ -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' +
-10
View File
@@ -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')
-44
View File
@@ -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')
})
})
-6
View File
@@ -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}}'
-8
View File
@@ -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('')
})
})
-23
View File
@@ -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 () {