Compare commits

...
42 Commits
Author SHA1 Message Date
Yang Jun af12665c5c docs: update docs for operators 2026-01-10 21:40:02 +08:00
Yang JunandGitHub 71aa1b1998 feat: export error types, resolving #837 (#840) 2025-11-22 00:27:44 +08:00
allcontributors[bot]GitHuballcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
350f95c8f7 docs: add rongjiecomputer as a contributor for code (#835)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2025-11-11 13:59:45 +08:00
Loo Rong JieandGitHub 955b7971c0 Support having new line and other whitespace after include filename (#834) 2025-11-11 13:58:12 +08:00
allcontributors[bot]GitHuballcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
8686876067 docs: add immerrr as a contributor for doc (#831)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2025-10-31 09:35:15 +08:00
immerrr againandGitHub 3a02eb12bf docs: update tutorial on operators and precedence (#830) 2025-10-31 09:34:05 +08:00
semantic-release-bot 906707833e chore(release): 10.24.0 [skip ci]
# [10.24.0](https://github.com/harttle/liquidjs/compare/v10.23.0...v10.24.0) (2025-10-27)

### Features

* **filters:** Add base64_encode and base64_decode filters for Shopify compatibility ([#828](https://github.com/harttle/liquidjs/issues/828)) ([86fc135](https://github.com/harttle/liquidjs/commit/86fc135d9ec0137689faf150535b9315e75ecc30))
2025-10-27 14:53:37 +00:00
allcontributors[bot]GitHuballcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
a2da822cb8 docs: add rosomri as a contributor for code (#829)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2025-10-27 22:42:15 +08:00
Omri RosnerandGitHub 86fc135d9e feat(filters): Add base64_encode and base64_decode filters for Shopify compatibility (#828)
* feat(filters): add base64 encode and decode

* fix: use Object.defineProperty for cross-platform btoa/atob mocking

* docs(filters): update docs

* docs(filters): update version
2025-10-27 22:40:31 +08:00
Yang JunandGitHub 5d953132e8 docs: add lambdatest to sponsors (#826) 2025-10-26 15:00:27 +08:00
allcontributors[bot]GitHuballcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2858271c8f docs: add skynetigor as a contributor for code (#825)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2025-10-23 22:23:52 +08:00
semantic-release-bot d22945ed5f chore(release): 10.23.0 [skip ci]
# [10.23.0](https://github.com/harttle/liquidjs/compare/v10.22.0...v10.23.0) (2025-10-23)

### Features

* Export specific tokens as types ([#824](https://github.com/harttle/liquidjs/issues/824)) ([4f7d2fd](https://github.com/harttle/liquidjs/commit/4f7d2fd84a8884e1009b13346d331a99b9721149))
2025-10-23 13:38:41 +00:00
Ihor PanasiukandGitHub 4f7d2fd84a feat: Export specific tokens as types (#824)
* Export specific tokens as types

* Update index.ts
2025-10-23 21:37:20 +08:00
semantic-release-bot 597ce7305f chore(release): 10.22.0 [skip ci]
# [10.22.0](https://github.com/harttle/liquidjs/compare/v10.21.1...v10.22.0) (2025-10-06)

### Bug Fixes

* math filters coerce invalid string to 0, [#813](https://github.com/harttle/liquidjs/issues/813) ([#819](https://github.com/harttle/liquidjs/issues/819)) ([e8e502c](https://github.com/harttle/liquidjs/commit/e8e502c5854c9649bf7611a671a068dc260011d1))

### Features

* allow context access in liquidMethodMissing, [#808](https://github.com/harttle/liquidjs/issues/808) ([#820](https://github.com/harttle/liquidjs/issues/820)) ([e551288](https://github.com/harttle/liquidjs/commit/e55128850e507687f9d85a012fc3a72ac2550f3b))
2025-10-06 14:45:55 +00:00
Yang Jun d7fa8ba5f1 chore: update node version for release workflow 2025-10-06 22:44:24 +08:00
Yang JunandGitHub 1b356d350d chore: fix Github artifact name (#821) 2025-10-06 22:32:34 +08:00
Yang JunandGitHub e55128850e feat: allow context access in liquidMethodMissing, #808 (#820) 2025-10-06 18:34:08 +08:00
Yang JunandGitHub e8e502c585 fix: math filters coerce invalid string to 0, #813 (#819) 2025-10-06 18:31:43 +08:00
allcontributors[bot]GitHuballcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
b8bc4db46c docs: add StreakingMan as a contributor for doc (#812)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2025-08-13 19:15:43 +08:00
裸奔狂甩丁丁andGitHub 68d500c18a docs: operators.md zh-cn translation (#811) 2025-08-13 19:14:47 +08:00
semantic-release-bot de12359bfd chore(release): 10.21.1 [skip ci]
## [10.21.1](https://github.com/harttle/liquidjs/compare/v10.21.0...v10.21.1) (2025-05-14)

### Bug Fixes

* block.super with strictVariables, [#806](https://github.com/harttle/liquidjs/issues/806) ([#807](https://github.com/harttle/liquidjs/issues/807)) ([025c40f](https://github.com/harttle/liquidjs/commit/025c40f0f2f13efa62193c61d2fa56943917ac3c))
2025-05-14 17:49:27 +00:00
Yang JunandGitHub 025c40f0f2 fix: block.super with strictVariables, #806 (#807) 2025-05-15 01:47:57 +08:00
Vlad GURDIGAandGitHub 2f414f8e40 docs: Fix formatting bug in echo.md (#805)
DISCLAIMER: This may not be the proper way to approach the issue.

Although the Markdown code is proper, on the website itself it is incorrectly rendered as "{{` and `}}".

The reason for the disclaimer above is that I’m imagining this may be an issue at the content rendering level, and my fix here is just a workaround of that issue. — Given this, I’ll not be offended if this PR of mine is rejected and closed. 🙂
2025-05-10 20:35:07 +08:00
Vlad GURDIGAandGitHub 40c52124d7 docs: Fix typo in options.md (#802)
Looks like just a typo.
2025-05-09 21:16:08 +08:00
allcontributors[bot]GitHuballcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
ae0c07e60b docs: add gurdiga as a contributor for doc (#804)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2025-05-09 20:37:59 +08:00
Vlad GURDIGAandGitHub 3b9202465e docs: A lil Markdown fix (#803)
Replaced square brackets with round ones in a link markup.
2025-05-09 20:36:49 +08:00
Harttle fc42ad7548 docs: update financial contributors 2025-04-03 21:27:48 +08:00
allcontributors[bot]andGitHub 050a7fc68e docs: add edh649 as a contributor for doc (#801) 2025-04-02 21:02:42 +08:00
Ed HantonandYang Jun 0fdc5c79da Update register-filters-tags.md
Improve documentation
2025-04-02 21:01:20 +08:00
semantic-release-bot 90d2ecb107 chore(release): 10.21.0 [skip ci]
# [10.21.0](https://github.com/harttle/liquidjs/compare/v10.20.3...v10.21.0) (2025-02-23)

### Features

* add find_index, has, and reject filters ([#799](https://github.com/harttle/liquidjs/issues/799)) ([0deb93e](https://github.com/harttle/liquidjs/commit/0deb93eeae4f530901e9a7d099bcc47207ad7385))
2025-02-23 14:59:29 +00:00
Bruno CarvalhoandGitHub 0deb93eeae feat: add find_index, has, and reject filters (#799)
* feat: add find_index, has, and reject filters

* Minor tweaks

* Change semantics of jekyllStyle, add more tests

* Some docs improvements
2025-02-23 22:57:17 +08:00
semantic-release-bot b0facc71f7 chore(release): 10.20.3 [skip ci]
## [10.20.3](https://github.com/harttle/liquidjs/compare/v10.20.2...v10.20.3) (2025-02-09)

### Bug Fixes

* empty tagToken.args since 10.20.0, fixes [#796](https://github.com/harttle/liquidjs/issues/796) ([38a0f51](https://github.com/harttle/liquidjs/commit/38a0f510b0a14baf35a368e9f07b536253394d06))
2025-02-09 14:51:09 +00:00
HarttleandYang Jun 5cb843f162 chore: migrate actions/upload-artifact to v4 2025-02-09 22:49:40 +08:00
HarttleandYang Jun 38a0f510b0 fix: empty tagToken.args since 10.20.0, fixes #796 2025-02-09 22:49:40 +08:00
Harttle 1a893f8023 docs: migrate to algolia app QJ35YOZTU4, #795 2025-02-09 22:12:04 +08:00
semantic-release-bot bf3bd54051 chore(release): 10.20.2 [skip ci]
## [10.20.2](https://github.com/harttle/liquidjs/compare/v10.20.1...v10.20.2) (2025-01-19)

### Bug Fixes

* consistent range syntax parsing, [#791](https://github.com/harttle/liquidjs/issues/791) ([a490a70](https://github.com/harttle/liquidjs/commit/a490a70da1ca2b479065c6618207bf4789db6b4f))
* context for group_by_exp/where_exp/find_exp, [#790](https://github.com/harttle/liquidjs/issues/790) ([a5070af](https://github.com/harttle/liquidjs/commit/a5070af3e4b4d1ae3b6398c6638b130e50e1cf6e))
2025-01-19 09:59:48 +00:00
HarttleandJun Yang a490a70da1 fix: consistent range syntax parsing, #791 2025-01-19 17:58:00 +08:00
HarttleandJun Yang a5070af3e4 fix: context for group_by_exp/where_exp/find_exp, #790 2025-01-19 17:53:25 +08:00
semantic-release-bot b070594fc7 chore(release): 10.20.1 [skip ci]
## [10.20.1](https://github.com/harttle/liquidjs/compare/v10.20.0...v10.20.1) (2025-01-04)

### Bug Fixes

* break/continue stops whole template, [#783](https://github.com/harttle/liquidjs/issues/783) ([5f1a4cf](https://github.com/harttle/liquidjs/commit/5f1a4cfdc9d6bde31ce86ddc88b8f4bdf52f7893))
* enumerate plain objects in where/where_exp, [#785](https://github.com/harttle/liquidjs/issues/785) ([#788](https://github.com/harttle/liquidjs/issues/788)) ([25ef104](https://github.com/harttle/liquidjs/commit/25ef104446731f4b6cb3a2e78f4d3b99efb635f4))
* preserveTimezones support for RFC2822 date, [#784](https://github.com/harttle/liquidjs/issues/784) ([59cf3c0](https://github.com/harttle/liquidjs/commit/59cf3c08dbc5f2e5b109ffcb5375ae738b5ac386))
2025-01-04 15:42:58 +00:00
Jun YangandGitHub 25ef104446 fix: enumerate plain objects in where/where_exp, #785 (#788) 2025-01-04 23:41:25 +08:00
HarttleandJun Yang 59cf3c08db fix: preserveTimezones support for RFC2822 date, #784 2025-01-04 22:46:07 +08:00
HarttleandJun Yang 5f1a4cfdc9 fix: break/continue stops whole template, #783 2025-01-04 22:15:39 +08:00
67 changed files with 9844 additions and 5005 deletions
+63
View File
@@ -721,6 +721,69 @@
"contributions": [
"code"
]
},
{
"login": "edh649",
"name": "Ed Hanton",
"avatar_url": "https://avatars.githubusercontent.com/u/527604?v=4",
"profile": "https://github.com/edh649",
"contributions": [
"doc"
]
},
{
"login": "gurdiga",
"name": "Vlad GURDIGA",
"avatar_url": "https://avatars.githubusercontent.com/u/53922?v=4",
"profile": "https://gurdiga.com",
"contributions": [
"doc"
]
},
{
"login": "StreakingMan",
"name": "裸奔狂甩丁丁",
"avatar_url": "https://avatars.githubusercontent.com/u/30397306?v=4",
"profile": "https://www.streakingman.com",
"contributions": [
"doc"
]
},
{
"login": "skynetigor",
"name": "Ihor Panasiuk",
"avatar_url": "https://avatars.githubusercontent.com/u/20903171?v=4",
"profile": "https://github.com/skynetigor",
"contributions": [
"code"
]
},
{
"login": "rosomri",
"name": "Omri Rosner",
"avatar_url": "https://avatars.githubusercontent.com/u/68001413?v=4",
"profile": "https://github.com/rosomri",
"contributions": [
"code"
]
},
{
"login": "immerrr",
"name": "immerrr again",
"avatar_url": "https://avatars.githubusercontent.com/u/579798?v=4",
"profile": "https://github.com/immerrr",
"contributions": [
"doc"
]
},
{
"login": "rongjiecomputer",
"name": "Loo Rong Jie",
"avatar_url": "https://avatars.githubusercontent.com/u/13115060?v=4",
"profile": "https://github.com/rongjiecomputer",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,
+3 -3
View File
@@ -28,13 +28,13 @@ jobs:
- name: Build
run: npm run build
- name: Archive artifacts
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: dist-${{ inputs.os }}
path: dist
- name: Archive npm failure logs
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
if: failure()
with:
name: npm-logs
name: npm-logs-${{ inputs.os }}
path: ~/.npm/_logs
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Archive npm failure logs
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
if: failure()
with:
name: npm-logs
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
branch: gh-pages
folder: docs/public
- name: Archive npm failure logs
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
if: failure()
with:
name: npm-logs
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
- name: Lint
run: npm run lint
- name: Archive npm failure logs
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
if: failure()
with:
name: npm-logs
+2 -2
View File
@@ -19,14 +19,14 @@ jobs:
with:
node-version: '20'
- name: Download artifacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: dist-${{ inputs.os }}
path: dist
- name: Check Performance
run: npm run perf:diff
- name: Archive npm failure logs
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
if: failure()
with:
name: npm-logs
+2 -2
View File
@@ -12,7 +12,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '14'
node-version: '22'
- name: Install Dependencies
run: npm ci
- name: Release
@@ -26,7 +26,7 @@ jobs:
npx semantic-release --dry-run
fi
- name: Archive npm failure logs
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
if: failure()
with:
name: npm-logs
+12 -12
View File
@@ -14,13 +14,13 @@ jobs:
node-versoin: 22
- os: ubuntu-latest
timezone: Etc/GMT
node-version: 20
- os: ubuntu-latest
timezone: Asia/Shanghai
node-version: 18
- os: ubuntu-latest
timezone: Asia/Shanghai
node-version: 16
- os: ubuntu-latest
timezone: Asia/Shanghai
node-version: 15
- os: ubuntu-latest
timezone: Asia/Shanghai
node-version: 14
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
@@ -34,17 +34,17 @@ jobs:
- name: Install dependencies
run: npm ci
- name: Download artifacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: dist-${{ matrix.os }}
path: dist
- name: Run Test
run: TZ=${{ matrix.timezone }} npm test
- name: Archive npm failure logs
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
if: failure()
with:
name: npm-logs
name: test-npm-logs-${{ matrix.os }}-${{ matrix.node-version }}
path: ~/.npm/_logs
demo:
name: Demo Check
@@ -59,15 +59,15 @@ jobs:
with:
node-version: 22
- name: Download artifacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: dist-ubuntu-latest
path: dist
- name: Run Demo Test
run: npm run test:demo
- name: Archive npm failure logs
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
if: failure()
with:
name: npm-logs
name: demo-npm-logs
path: ~/.npm/_logs
+64
View File
@@ -1,3 +1,67 @@
# [10.24.0](https://github.com/harttle/liquidjs/compare/v10.23.0...v10.24.0) (2025-10-27)
### Features
* **filters:** Add base64_encode and base64_decode filters for Shopify compatibility ([#828](https://github.com/harttle/liquidjs/issues/828)) ([86fc135](https://github.com/harttle/liquidjs/commit/86fc135d9ec0137689faf150535b9315e75ecc30))
# [10.23.0](https://github.com/harttle/liquidjs/compare/v10.22.0...v10.23.0) (2025-10-23)
### Features
* Export specific tokens as types ([#824](https://github.com/harttle/liquidjs/issues/824)) ([4f7d2fd](https://github.com/harttle/liquidjs/commit/4f7d2fd84a8884e1009b13346d331a99b9721149))
# [10.22.0](https://github.com/harttle/liquidjs/compare/v10.21.1...v10.22.0) (2025-10-06)
### Bug Fixes
* math filters coerce invalid string to 0, [#813](https://github.com/harttle/liquidjs/issues/813) ([#819](https://github.com/harttle/liquidjs/issues/819)) ([e8e502c](https://github.com/harttle/liquidjs/commit/e8e502c5854c9649bf7611a671a068dc260011d1))
### Features
* allow context access in liquidMethodMissing, [#808](https://github.com/harttle/liquidjs/issues/808) ([#820](https://github.com/harttle/liquidjs/issues/820)) ([e551288](https://github.com/harttle/liquidjs/commit/e55128850e507687f9d85a012fc3a72ac2550f3b))
## [10.21.1](https://github.com/harttle/liquidjs/compare/v10.21.0...v10.21.1) (2025-05-14)
### Bug Fixes
* block.super with strictVariables, [#806](https://github.com/harttle/liquidjs/issues/806) ([#807](https://github.com/harttle/liquidjs/issues/807)) ([025c40f](https://github.com/harttle/liquidjs/commit/025c40f0f2f13efa62193c61d2fa56943917ac3c))
# [10.21.0](https://github.com/harttle/liquidjs/compare/v10.20.3...v10.21.0) (2025-02-23)
### Features
* add find_index, has, and reject filters ([#799](https://github.com/harttle/liquidjs/issues/799)) ([0deb93e](https://github.com/harttle/liquidjs/commit/0deb93eeae4f530901e9a7d099bcc47207ad7385))
## [10.20.3](https://github.com/harttle/liquidjs/compare/v10.20.2...v10.20.3) (2025-02-09)
### Bug Fixes
* empty tagToken.args since 10.20.0, fixes [#796](https://github.com/harttle/liquidjs/issues/796) ([38a0f51](https://github.com/harttle/liquidjs/commit/38a0f510b0a14baf35a368e9f07b536253394d06))
## [10.20.2](https://github.com/harttle/liquidjs/compare/v10.20.1...v10.20.2) (2025-01-19)
### Bug Fixes
* consistent range syntax parsing, [#791](https://github.com/harttle/liquidjs/issues/791) ([a490a70](https://github.com/harttle/liquidjs/commit/a490a70da1ca2b479065c6618207bf4789db6b4f))
* context for group_by_exp/where_exp/find_exp, [#790](https://github.com/harttle/liquidjs/issues/790) ([a5070af](https://github.com/harttle/liquidjs/commit/a5070af3e4b4d1ae3b6398c6638b130e50e1cf6e))
## [10.20.1](https://github.com/harttle/liquidjs/compare/v10.20.0...v10.20.1) (2025-01-04)
### Bug Fixes
* break/continue stops whole template, [#783](https://github.com/harttle/liquidjs/issues/783) ([5f1a4cf](https://github.com/harttle/liquidjs/commit/5f1a4cfdc9d6bde31ce86ddc88b8f4bdf52f7893))
* enumerate plain objects in where/where_exp, [#785](https://github.com/harttle/liquidjs/issues/785) ([#788](https://github.com/harttle/liquidjs/issues/788)) ([25ef104](https://github.com/harttle/liquidjs/commit/25ef104446731f4b6cb3a2e78f4d3b99efb635f4))
* preserveTimezones support for RFC2822 date, [#784](https://github.com/harttle/liquidjs/issues/784) ([59cf3c0](https://github.com/harttle/liquidjs/commit/59cf3c08dbc5f2e5b109ffcb5375ae738b5ac386))
# [10.20.0](https://github.com/harttle/liquidjs/compare/v10.19.1...v10.20.0) (2024-12-28)
+34 -27
View File
@@ -53,6 +53,7 @@ For more details, refer to the [Setup Guide][setup].
## Who's Using LiquidJS?
- [Eleventy](https://www.11ty.dev/): Eleventy, a simpler static site generator.
- [Github Docs](https://github.com/github/docs): The open-source repo for docs.github.com.
- [Opensense](https://www.opensense.com/): The smarter way to send email.
- [Directus](https://docs.directus.io/): an instant REST+GraphQL API and intuitive no-code data collaboration app for any SQL database.
- [Semgrep](https://github.com/returntocorp/semgrep): Lightweight static analysis for many languages.
@@ -71,33 +72,30 @@ Feel free to create a PR or contact me to add your use case into this list!
If you personally love LiquidJS or it's benefiting your business, please consider financially support us via [GitHub Sponsors](https://github.com/sponsors/harttle). Special thanks to our sponsors!
<!-- 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="https://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>
<td align="center" valign="top" width="14.28%"><a href="https://customer.io/"><img src="https://avatars.githubusercontent.com/u/1152079?v=4&s=100" width="100px;" alt="Customer IO"/><br /><sub><b>Customer IO</b></sub></a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/15fathoms"><img src="https://avatars.githubusercontent.com/u/79156039?v=4&s=100" width="100px;" alt="Emmanuel Cartelli"/><br /><sub><b>Emmanuel Cartelli</b></sub></a><br /></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/microsoft"><img src="https://avatars.githubusercontent.com/u/6154722?v=4&s=100" width="100px;" alt="Microsoft"/><br /><sub><b>Microsoft</b></sub></a><br /></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://www.pakstyle.pk/"><img src="https://images.opencollective.com/pakstyle/2b81605/logo/256.png?height=100" width="100px;" alt="PakStyle.pk"/><br /><sub><b>PakStyle.pk</b></sub></a></td>
<td align="center" valign="top" width="14.28%"><a href="https://syntax.fm/"><img src="https://avatars.githubusercontent.com/u/130389858?v=4&s=100" width="100px;" alt="Syntax Podcast"/><br /><sub><b>Syntax Podcast</b></sub></a></td>
<td align="center" valign="top" width="14.28%"><a href="https://opencollective.com/cartelli-emmanuel"><img src="https://images.opencollective.com/cartelli-emmanuel/avatar/256.png?height=100" width="100px;" alt="Cartelli Emmanuel"/><br /><sub><b>Cartelli Emmanuel</b></sub></a></td>
</tr>
</tbody>
</table>
<p align="center" style="line-height: 2.5;">
<a href="https://www.11ty.dev/" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://avatars.githubusercontent.com/u/35147177?v=4&s=100" height="80" style="vertical-align: middle;" alt="Eleventy" title="Eleventy"/></a>
<a href="https://www.opensense.com/" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://images.opencollective.com/opensense-inc/bf840ae/logo/256.png?height=100" height="80" style="vertical-align: middle;" alt="Opensense Inc." title="Opensense"/></a>
<a href="https://github.com/microsoft" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://avatars.githubusercontent.com/u/6154722?v=4&s=100" height="80" style="vertical-align: middle;" alt="Microsoft" title="Microsoft"/></a>
<a href="https://sentry.io/" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://avatars.githubusercontent.com/u/1396951?v=4&s=100" height="80" style="vertical-align: middle;" alt="Sentry" title="Sentry"/></a>
<a href="https://www.checkoutblocks.com/" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://avatars.githubusercontent.com/u/114603307?v=4&s=100" height="80" style="vertical-align: middle;" alt="Checkout Blocks" title="Checkout Blocks"/></a>
<a href="https://customer.io/" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://avatars.githubusercontent.com/u/1152079?v=4&s=100" height="80" style="vertical-align: middle;" alt="Customer IO" title="Customer IO"/></a>
<a href="https://syntax.fm/" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://avatars.githubusercontent.com/u/130389858?v=4&s=100" height="80" style="vertical-align: middle;" alt="Syntax Podcast" title="Syntax Podcast"/></a>
<br/>
<a href="https://www.lambdatest.com/?utm_source=liquidjs&utm_medium=sponsor" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://www.lambdatest.com/blue-logo.png" width="240" style="vertical-align: middle;" alt="LambdaTest" title="LambdaTest"/></a>
<a href="https://chudovo.com/" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://images.opencollective.com/Chudovo/avatar/256.png?height=100" width="160" style="vertical-align: middle;background: white;padding: 8px 16px;" alt="Chudovo" title="Chudovo"/></a>
<a href="https://dailycontributors.com/" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://images.opencollective.com/dailycontributors/3c2e057/logo/256.png?height=50&width=100" width="120" style="vertical-align: middle;" alt="Dailycontributors" title="Dailycontributors"/></a>
<a href="https://www.pakstyle.pk/" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://images.opencollective.com/pakstyle/2b81605/logo/256.png?height=100" height="80" style="vertical-align: middle;" alt="PakStyle.pk" title="PakStyle.pk"/></a>
<a href="https://www.escorta.com/" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://images.opencollective.com/escortacom/avatar/256.png?height=100" height="45" style="vertical-align: middle;" alt="EscortA.com" title="EscortA.com"/></a>
<br/>
<a href="https://opencollective.com/touchless" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://images.opencollective.com/touchless/273bc74/logo/256.png?height=100" height="80" style="vertical-align: middle;" alt="Touchless" title="Touchless"/></a>
<a href="https://www.dropkiq.com/" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://images.opencollective.com/1bertlol/43a8ea8/logo/256.png?height=100" height="80" style="vertical-align: middle;" alt="Dropkiq" title="Dropkiq"/></a>
<a href="https://about.me/peterdehaan" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://avatars2.githubusercontent.com/u/557895?v=4&s=100" height="80" style="vertical-align: middle;" alt="Peter deHaan" title="Peter deHaan"/></a>
<a href="https://github.com/coni2k" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://avatars0.githubusercontent.com/u/1284601?v=4&s=100" height="80" style="vertical-align: middle;" alt="Serkan Holat" title="Serkan Holat"/></a>
<a href="https://github.com/amit777" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://avatars0.githubusercontent.com/u/2703309?v=4&s=100" height="80" style="vertical-align: middle;" alt="amit777" title="amit777"/></a>
<a href="https://opencollective.com/khaled-salem" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://images.opencollective.com/khaled-salem/avatar/256.png?height=256" height="80" style="vertical-align: middle;" alt="Khaled Salem" title="Khaled Salem"/></a>
<a href="https://github.com/15fathoms" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://avatars.githubusercontent.com/u/79156039?v=4&s=100" height="80" style="vertical-align: middle;" alt="Emmanuel Cartelli" title="Emmanuel Cartelli"/></a>
<a href="https://opencollective.com/cartelli-emmanuel" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://images.opencollective.com/cartelli-emmanuel/avatar/256.png?height=100" height="80" style="vertical-align: middle;" alt="Cartelli Emmanuel" title="Cartelli Emmanuel"/></a>
</p>
<!-- FINANCIAL-CONTRIBUTORS-END -->
## Contributors ✨
@@ -210,6 +208,15 @@ Want to contribute? see [Contribution Guidelines][contribution]. Thanks goes to
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://tovd.dev"><img src="https://avatars.githubusercontent.com/u/35376389?v=4?s=100" width="100px;" alt="Tim van Dam"/><br /><sub><b>Tim van Dam</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=timvandam" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/edh649"><img src="https://avatars.githubusercontent.com/u/527604?v=4?s=100" width="100px;" alt="Ed Hanton"/><br /><sub><b>Ed Hanton</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=edh649" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://gurdiga.com"><img src="https://avatars.githubusercontent.com/u/53922?v=4?s=100" width="100px;" alt="Vlad GURDIGA"/><br /><sub><b>Vlad GURDIGA</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=gurdiga" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.streakingman.com"><img src="https://avatars.githubusercontent.com/u/30397306?v=4?s=100" width="100px;" alt="裸奔狂甩丁丁"/><br /><sub><b>裸奔狂甩丁丁</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=StreakingMan" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/skynetigor"><img src="https://avatars.githubusercontent.com/u/20903171?v=4?s=100" width="100px;" alt="Ihor Panasiuk"/><br /><sub><b>Ihor Panasiuk</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=skynetigor" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rosomri"><img src="https://avatars.githubusercontent.com/u/68001413?v=4?s=100" width="100px;" alt="Omri Rosner"/><br /><sub><b>Omri Rosner</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=rosomri" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/immerrr"><img src="https://avatars.githubusercontent.com/u/579798?v=4?s=100" width="100px;" alt="immerrr again"/><br /><sub><b>immerrr again</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=immerrr" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rongjiecomputer"><img src="https://avatars.githubusercontent.com/u/13115060?v=4?s=100" width="100px;" alt="Loo Rong Jie"/><br /><sub><b>Loo Rong Jie</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=rongjiecomputer" title="Code">💻</a></td>
</tr>
</tbody>
</table>
-1
View File
@@ -20,7 +20,6 @@ awk '/ALL-CONTRIBUTORS-LIST:START/{flag=1;next}/ALL-CONTRIBUTORS-LIST:END/{flag=
# 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
+2 -2
View File
@@ -30,8 +30,8 @@ prismjs:
tab_replace: ""
algolia:
applicationID: 0X19J927JZ
apiKey: 533161d821384919672e4ce8a39451b3
applicationID: QJ35YOZTU4
apiKey: 8c6cbb824b4c5023f0bb2ef29e228bef
indexName: liquidjs
twitter: harttleharttle
github: harttle/liquidjs
+6
View File
@@ -51,10 +51,14 @@ filters:
escape_once: escape_once.html
find: find.html
find_exp: find_exp.html
find_index: find_index.html
find_index_exp: find_index_exp.html
first: first.html
floor: floor.html
group_by: group_by.html
group_by_exp: group_by_exp.html
has: has.html
has_exp: has_exp.html
inspect: inspect.html
join: join.html
json: json.html
@@ -72,6 +76,8 @@ filters:
push: push.html
prepend: prepend.html
raw: raw.html
reject: reject.html
reject_exp: reject_exp.html
remove: remove.html
remove_first: remove_first.html
remove_last: remove_last.html
+27
View File
@@ -0,0 +1,27 @@
---
title: base64_decode
---
{% since %}v10.24.0{% endsince %}
Decodes a Base64-formatted string back to its original text.
Input
```liquid
{{ "b25lIHR3byB0aHJlZQ==" | base64_decode }}
```
Output
```text
one two three
```
Input
```liquid
{{ "SGVsbG8sIFdvcmxkISBAIyQl" | base64_decode }}
```
Output
```text
Hello, World! @#$%
```
+27
View File
@@ -0,0 +1,27 @@
---
title: base64_encode
---
{% since %}v10.24.0{% endsince %}
Encodes a string into Base64 format.
Input
```liquid
{{ "one two three" | base64_encode }}
```
Output
```text
b25lIHR3byB0aHJlZQ==
```
Input
```liquid
{{ "Hello, World! @#$%" | base64_encode }}
```
Output
```text
SGVsbG8sIFdvcmxkISBAIyQl
```
+25
View File
@@ -0,0 +1,25 @@
---
title: find_index
---
{% since %}v10.21.0{% endsince %}
Return the 0-based index of the first object in an array for which the queried attribute has the given value or return `nil` if no item in the array satisfies the given criteria. For the following `members` array:
```javascript
const members = [
{ graduation_year: 2013, name: 'Jay' },
{ graduation_year: 2014, name: 'John' },
{ graduation_year: 2014, name: 'Jack' }
]
```
Input
```liquid
{{ members | find_index: "graduation_year", 2014 | json }}
```
Output
```text
1
```
+25
View File
@@ -0,0 +1,25 @@
---
title: find_index_exp
---
{% since %}v10.21.0{% endsince %}
Return the 0-based index of the first object in an array for which the given expression evaluates to true or return `nil` if no item in the array satisfies the evaluated expression.
```javascript
const members = [
{ graduation_year: 2013, name: 'Jay' },
{ graduation_year: 2014, name: 'John' },
{ graduation_year: 2014, name: 'Jack' }
]
```
Input
```liquid
{{ members | find_index_exp: "item", "item.graduation_year == 2014" | json }}
```
Output
```text
1
```
+25
View File
@@ -0,0 +1,25 @@
---
title: has
---
{% since %}v10.21.0{% endsince %}
Return `true` if the array includes an item for which the queried attribute has the given value or return `false` if no item in the array satisfies the given criteria. For the following `members` array:
```javascript
const members = [
{ graduation_year: 2013, name: 'Jay' },
{ graduation_year: 2014, name: 'John' },
{ graduation_year: 2014, name: 'Jack' }
]
```
Input
```liquid
{{ members | has: "graduation_year", 2014 | json }}
```
Output
```text
true
```
+25
View File
@@ -0,0 +1,25 @@
---
title: has_exp
---
{% since %}v10.21.0{% endsince %}
Return `true` if an item exists in an array for which the given expression evaluates to true or return `false` if no item in the array satisfies the evaluated expression.
```javascript
const members = [
{ graduation_year: 2013, name: 'Jay' },
{ graduation_year: 2014, name: 'John' },
{ graduation_year: 2014, name: 'Jack' }
]
```
Input
```liquid
{{ members | has_exp: "item", "item.graduation_year == 2014" | json }}
```
Output
```text
true
```
+1
View File
@@ -15,5 +15,6 @@ HTML/URI | escape, escape_once, url_encode, url_decode, strip_html, newline_to_b
Array | slice, map, sort, sort_natural, uniq, where, where_exp, group_by, group_by_exp, find, find_exp, first, last, join, reverse, concat, compact, size, push, pop, shift, unshift
Date | date, date_to_xmlschema, date_to_rfc822, date_to_string, date_to_long_string
Misc | default, json, jsonify, inspect, raw, to_integer
Base64 | base64_encode, base64_decode
[shopify/liquid]: https://github.com/Shopify/liquid
+118
View File
@@ -0,0 +1,118 @@
---
title: reject
---
{% since %}v10.21.0{% endsince %}
Creates an array excluding the objects with a given property value, or excluding [truthy][truthy] values by default when a property is not given.
In this example, assume you have a list of products and you want to filter out kitchen products. Using `reject`, you can create an array excluding only the products that have a `"type"` of `"kitchen"`.
Input
```liquid
All products:
{% for product in products %}
- {{ product.title }}
{% endfor %}
{% assign non_kitchen_products = products | reject: "type", "kitchen" %}
Kitchen products:
{% for product in non_kitchen_products %}
- {{ product.title }}
{% endfor %}
```
Output
```text
All products:
- Vacuum
- Spatula
- Television
- Garlic press
Kitchen products:
- Vacuum
- Television
```
Say instead you have a list of products and you want to exclude taxable products. You can `reject` with a property name but no target value to reject all products with a [truthy][truthy] `"taxable"` value.
Input
```liquid
All products:
{% for product in products %}
- {{ product.title }}
{% endfor %}
{% assign not_taxed_products = products | reject: "taxable" %}
Available products:
{% for product in not_taxed_products %}
- {{ product.title }}
{% endfor %}
```
Output
```text
All products:
- Vacuum
- Spatula
- Television
- Garlic press
Available products:
- Spatula
- Television
```
Additionally, `property` can be any valid Liquid variable expression as used in output syntax, except that the scope of this expression is within each item. For the following `products` array:
```javascript
const products = [
{ meta: { details: { class: 'A' } }, order: 1 },
{ meta: { details: { class: 'B' } }, order: 2 },
{ meta: { details: { class: 'B' } }, order: 3 }
]
```
Input
```liquid
{% assign selected = products | reject: 'meta.details["class"]', "B" %}
{% for item in selected -%}
- {{ item.order }}
{% endfor %}
```
Output
```text
- 1
```
## Jekyll style
{% since %}v10.21.0{% endsince %}
For Liquid users migrating from Jekyll, there's a `jekyllWhere` option to mimic the behavior of Jekyll's `where` filter. This option is set to `false` by default. When enabled, if `property` is an array, the target value is matched using `Array.includes` instead of `==`, which is particularly useful for excluding tags.
```javascript
const pages = [
{ tags: ["cat", "food"], title: 'Cat Food' },
{ tags: ["dog", "food"], title: 'Dog Food' },
]
```
Input
```liquid
{% assign selected = pages | reject: 'tags', "cat" %}
{% for item in selected -%}
- {{ item.title }}
{% endfor %}
```
Output
```text
Dog Food
```
[truthy]: ../tutorials/truthy-and-falsy.html
+37
View File
@@ -0,0 +1,37 @@
---
title: reject_exp
---
{% since %}v10.21.0{% endsince %}
Select all the objects in an array where the expression is false. In this example, assume you have a list of products and you want to hide your kitchen products. Using `reject_exp`, you can create an array that omits only the products that have a `"type"` of `"kitchen"`.
Input
```liquid
All products:
{% for product in products %}
- {{ product.title }}
{% endfor %}
{% assign non_kitchen_products = products | reject_exp: "item", "item.type == 'kitchen'" %}
Kitchen products:
{% for product in non_kitchen_products %}
- {{ product.title }}
{% endfor %}
```
Output
```text
All products:
- Vacuum
- Spatula
- Television
- Garlic press
Kitchen products:
- Vacuum
- Television
```
[truthy]: ../tutorials/truthy-and-falsy.html
+5 -3
View File
@@ -37,6 +37,7 @@ Kitchen products:
```
Say instead you have a list of products and you only want to show those that are available to buy. You can `where` with a property name but no target value to include all products with a [truthy][truthy] `"available"` value.
As a special case, the same will happen if the target value is given but evaluates to `undefined`.
Input
```liquid
@@ -70,7 +71,6 @@ The `where` filter can also be used to find a single object in an array when com
Input
```liquid
{% assign new_shirt = products | where: "type", "shirt" | first %}
Featured product: {{ new_shirt.title }}
```
@@ -105,9 +105,11 @@ Output
## Jekyll style
{% since %}v10.19.0{% endsince %}
{% since %}v10.21.0{% endsince %}
For Liquid users migrating from Jekyll, there's a `jekyllWhere` option to mimic the behavior of Jekyll's `where` filter. This option is set to `false` by default. When enabled, if `property` is an array, the target value is matched using `Array.includes` instead of `==`, which is particularly useful for filtering tags.
For Liquid users migrating from Jekyll, there's a `jekyllWhere` option to mimic the behavior of Jekyll's `where` filter. This option is set to `false` by default. When enabled, if `property` is an array, the target value is matched using `Array.includes` instead of `==`, which is particularly useful for filtering tags. Additionally, a target value of `undefined` is treated normally, entries matched are exactly those which are themselves `undefined`.
This option affects other array selection filters as well, such as `reject` and `find`.
```javascript
const pages = [
+1
View File
@@ -10,6 +10,7 @@ const urlsToCache = [
]
const blackList = [
/chrome-extension:/,
/algolia.net/,
/google-analytics.com.*collect/
]
+1 -1
View File
@@ -4,7 +4,7 @@ title: Echo
{% since %}v9.31.0{% endsince %}
Outputs an expression in the rendered HTML. This is identical to wrapping an expression in `{{` and `}}`, but works inside liquid tags and supports filters.
Outputs an expression in the rendered HTML. This is identical to wrapping an expression in <code>{{</code> and <code>}}</code>, but works inside liquid tags and supports filters.
## echo
@@ -20,7 +20,11 @@ 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, especially when LiquidJS is benefiting your business, please consider contribute on [GitHub Sponsors](https://github.com/sponsors/harttle) or [Open Collective][oc].
I'll add all financial contributors into [README.md](https://github.com/harttle/liquidjs#financial-support) and it'll be also shown on https://liquidjs.com after next GitHub Actions build.
If I'm missing anything or you observed it not working, please don't hesitate to file an issue or find me via email (harttleharttle at gmail).
[oc]: https://opencollective.com/liquidjs/contribute/backer-10665/checkout
[shopify/liquid]: https://shopify.github.io/liquid/
+51 -4
View File
@@ -5,17 +5,64 @@ title: Operators
LiquidJS operators are very simple and different. There're 2 types of operators supported:
* Comparison operators: `==`, `!=`, `>`, `<`, `>=`, `<=`
* Logic operators: `or`, `and`, `contains`
* Logic operators: `not`, `or`, `and`, `contains`
Thus numerical operators are not supported and you cannot even plus two numbers like this `{% raw %}{{a + b}}{% endraw %}`, instead we need a filter `{% raw %}{{ a | plus: b}}{% endraw %}`. Actually `+` is a valid variable name in LiquidJS.
## Logic Operators
### not
Negates a condition. Returns `true` if the condition is false, and `false` if the condition is true.
Input
```liquid
{% if not user.active %}
User is inactive
{% endif %}
```
### and
Returns `true` if both conditions are true.
Input
```liquid
{% if user.age >= 18 and user.verified %}
Access granted
{% endif %}
```
### or
Returns `true` if at least one condition is true.
Input
```liquid
{% if user.isAdmin or user.isModerator %}
You have elevated privileges
{% endif %}
```
### contains
Checks if a string contains a substring, or if an array contains an element.
Input
```liquid
{% if product.title contains "Pack" %}
This is a pack
{% endif %}
```
## Precedence
1. Comparison operators. All comparison operations have the same precedence and higher than logic operators.
2. Logic operators. All logic operators have the same precedence.
1. Comparison operators, and `contains`. All comparison operators alongside `contains` have the same (highest) precedence.
2. `not` operator. It has slightly more precedence than `or` and `and`.
3. `or` and `and` operators. These logic operators have the same (lowest) precedence.
## Associativity
Logic operators are evaluated from right to left, see [shopify docs][operator-order].
[operator-order]: https://help.shopify.com/en/themes/liquid/basics/operators#order-of-operations
[operator-order]: https://shopify.dev/docs/api/liquid/basics#order-of-operations
+2 -2
View File
@@ -116,7 +116,7 @@ it defaults to false. For example, when set to true, a blank string would evalu
**preserveTimezones** is a boolean effects only literal timestamps. When set to `true`, all literal timestamps will remain the same when output. This is a parser option, so Date objects passed to LiquidJS as data will not be affected. Note that `preserveTimezones` has a higher priority than `timezoneOffset`.
**dateFormat** is used to specify a default format to output dates. `%A, %B %-e, %Y at %-l:%M %P %z` will be used if not specified. For example, set `dateFormat: %Y-%m-%dT%H:%M:%S:%LZ` to output all dates in [JavaScript Date.toJson()][https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toJSON] format.
**dateFormat** is used to specify a default format to output dates. `%A, %B %-e, %Y at %-l:%M %P %z` will be used if not specified. For example, set `dateFormat: %Y-%m-%dT%H:%M:%S:%LZ` to output all dates in [JavaScript Date.toJson()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toJSON) format.
## Trimming
@@ -146,7 +146,7 @@ Nonexistent tags always throw errors during parsing and this behavior cannot be
## Parameter Order
Parameter orders are ignored by default, for ea `{% for i in (1..8) reversed limit:3 %}` will always perform `limit` before `reversed`, even if `reversed` occurs before `limit`. To make parameter order respected, set **orderedFilterParameters** to `true`. Its default value is `false`.
Parameter orders are ignored by default, for example `{% for i in (1..8) reversed limit:3 %}` will always perform `limit` before `reversed`, even if `reversed` occurs before `limit`. To make parameter order respected, set **orderedFilterParameters** to `true`. Its default value is `false`.
[liquid]: /api/classes/Liquid.html
[caching]: ./caching.html
@@ -10,7 +10,7 @@ import { Value, TagToken, Context, Emitter, TopLevelToken } from 'liquidjs'
engine.registerTag('upper', {
parse: function(tagToken: TagToken, remainTokens: TopLevelToken[]) {
this.value = new Value(token.args, liquid)
this.value = new Value(tagToken.args, engine)
},
render: function*(ctx: Context) {
const str = yield this.value.value(ctx); // 'alice'
@@ -80,4 +80,4 @@ function disabledFilter(name) {
}
}
engine.registerFilter('plus', disabledFilter('plus'));
```
```
+51 -4
View File
@@ -5,17 +5,64 @@ title: 运算符
LiquidJS 运算符非常简单也很特别,只支持两类运算符:
* 比较运算符:`==`, `!=`, `>`, `<`, `>=`, `<=`
* 逻辑运算符:`or`, `and`, `contains`
* 逻辑运算符:`not`, `or`, `and`, `contains`
因此普通的数学运算是不支持的,比如 `{% raw %}{{a + b}}{% endraw %}`。它的替代方案是过滤器 `{% raw %}{{ a | plus: b}}{% endraw %}`。事实上 `+` 在 LiquidJS 中是一个合法的变量名。
## 逻辑运算符
### not
对条件取反。如果条件为假则返回 `true`,如果条件为真则返回 `false`
输入
```liquid
{% if not user.active %}
用户未激活
{% endif %}
```
### and
当两个条件都为真时返回 `true`
输入
```liquid
{% if user.age >= 18 and user.verified %}
允许访问
{% endif %}
```
### or
当至少一个条件为真时返回 `true`
输入
```liquid
{% if user.isAdmin or user.isModerator %}
您拥有提升的权限
{% endif %}
```
### contains
检查字符串是否包含子字符串,或数组是否包含元素。
输入
```liquid
{% if product.title contains "Pack" %}
这是一个套装
{% endif %}
```
## 优先级
1. 比较运算符。所有比较运算符具有同样的优先级,且高于逻辑运算符
2. 逻辑运算符。所有逻辑运算符具有同样的有衔接
1. 比较运算符`contains`。所有比较运算符和 `contains` 具有同样的(最高)优先级
2. `not` 运算符。它的优先级略高于 `or``and`
3. `or``and` 运算符。这些逻辑运算符具有同样的(最低)优先级。
## 结合性
逻辑运算符是结合的,所以连续的逻辑运算时计算顺序是从右向左,参考 [Shopify][operator-order] 的文档。
逻辑运算符是结合的,所以连续的逻辑运算时计算顺序是从右向左,参考 [Shopify][operator-order] 的文档。
[operator-order]: https://help.shopify.com/en/themes/liquid/basics/operators#order-of-operations
+15 -15
View File
@@ -1,5 +1,5 @@
<header id="banner" class="wrapper">
<div class="inner">
<div class="inner inner-content">
<h2 id="banner-title">{{ page.subtitle }}</h2>
<div id="banner-start">
<code id="banner-start-command">npm install liquidjs</code><a id="banner-start-link" href="./tutorials/setup.html"><i class="icon-arrow-right"></i></a>
@@ -9,7 +9,7 @@
</header>
<div id="intro-news-list">
<div class="wrapper">
<div class="inner">
<div class="inner inner-content">
<div id="intro-news-flex">
{% for news in site.data.news %}
<a href="{{news.url}}" class="intro-news-wrap">
@@ -23,7 +23,7 @@
</div>
<div id="content-wrap">
<div class="wrapper">
<div class="inner">
<div class="inner inner-content">
{{ page.content }}
<div id="intro-cmd-wrap" class="highlight typescript"><pre><span class="line"><span class="keyword">import</span> { Liquid } <span class="keyword">from</span> <span class="string">'liquidjs'</span></span><br><span class="line"><span class="keyword">const</span> engine = <span class="keyword">new</span> Liquid()</span><br><span class="line"><span class="keyword">const</span> tpl = engine.parse(<span class="string">'Welcome to {% raw %}{{v}}{% endraw %}!'</span>)</span><br><span class="line">engine.render(tpl, {<span class="attr">v</span>: <span class="string">"Liquid"</span>}).then(<span class="built_in">console</span>.log)</span><br><span class="line"><span class="comment">// Outputs "Welcome to Liquid!"</span></span><br></pre></div>
<div id="intro-get-started-wrap">
@@ -32,20 +32,9 @@
</div>
</div>
</div>
<div id="contributors-wrap">
<div class="wrapper">
<div class="inner">
<h3>{{__('index.contributors.title')}}</h3>
<p class="description">{{__('index.contributors.description')}}</p>
<div class="contributors">
{{ partial('partial/all-contributors') }}
</div>
</div>
</div>
</div>
<div id="sponsors-wrap">
<div class="wrapper">
<div class="inner">
<div class="inner inner-content">
<h3>{{__('index.sponsors.title')}}</h3>
<p class="description">{{__('index.sponsors.description')}}</p>
<div class="contributors">
@@ -54,3 +43,14 @@
</div>
</div>
</div>
<div id="contributors-wrap">
<div class="wrapper">
<div class="inner inner-content">
<h3>{{__('index.contributors.title')}}</h3>
<p class="description">{{__('index.contributors.description')}}</p>
<div class="contributors">
{{ partial('partial/all-contributors') }}
</div>
</div>
</div>
</div>
+2 -1
View File
@@ -9,7 +9,8 @@
<script>
document.getElementById('search-input-wrap').classList.add('on');
docsearch({
apiKey: '01f36cc168657a26a385308b9e721bc6',
appId: '{{ config.algolia.applicationID }}',
apiKey: '{{ config.algolia.apiKey }}',
indexName: 'liquidjs',
inputSelector: '#search-input',
debug: false
+5 -1
View File
@@ -54,7 +54,11 @@ a
.inner
@media screen
padding: 0 gutter-width
margin: 0 gutter-width
.inner.inner-content
margin: 0 32px
@media mq-tablet
margin: 0 48px
#content-wrap
background: var(--color-content-bg)
+21 -17
View File
@@ -66,6 +66,10 @@
.intro-news-wrap
padding: 24px 30px
text-decoration: none
&:first-child
padding-left: 0
&:last-child
padding-right: 0
&:hover
background-color: var(--color-link-hover)
@media mq-normal
@@ -105,7 +109,7 @@
text-align: center
@media mq-normal
text-align: left
padding-left: 70px
padding-left: 48px
.intro-feature-icon
color: var(--color-link)
@@ -116,7 +120,7 @@
margin-bottom: 26px
position: absolute
top: 0
left: 20px
left: 0
font-size: 24px
width: @font-size
@@ -177,9 +181,11 @@
border-bottom: 1px solid #161d24
margin: -1px 0
.inner
margin: 48px 16px
margin-top: 48px
margin-bottom: 48px
@media mq-tablet
margin: 64px 32px
margin-top: 64px
margin-bottom: 64px
h3
color: #fff
font-size: 32px
@@ -192,20 +198,7 @@
text-decoration: none
#contributors-wrap
.description a
color: #fff
&:hover
background: #fff
color: var(--color-navy-lighter)
#sponsors-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,10 +220,20 @@
background: #fff
color: var(--color-navy-lighter)
#sponsors-wrap
border: none
overflow: hidden;
.description a
color: #fff
&:hover
background: #fff
color: var(--color-navy-lighter)
.contributors
tr
display: flex
flex-wrap: wrap
justify-content: center;
td
margin: 10px 2px 0
a img
@@ -240,3 +243,4 @@
background: var(--color-gray)
border-radius: 50%
margin-bottom: 10px
object-fit: contain
+1
View File
@@ -1,6 +1,7 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
maxWorkers: '50%',
testEnvironment: 'node',
testMatch: ['**/*.spec.ts'],
collectCoverageFrom: [
+8340 -4794
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "liquidjs",
"version": "10.20.0",
"version": "10.24.0",
"sideEffects": false,
"description": "A simple, expressive and safe Shopify / Github Pages compatible template engine in pure JavaScript.",
"main": "dist/liquid.node.js",
@@ -12,7 +12,7 @@
},
"types": "dist/index.d.ts",
"engines": {
"node": ">=14"
"node": ">=16"
},
"scripts": {
"lint": "eslint \"**/*.mjs\" \"**/*.ts\" .",
+8
View File
@@ -45,6 +45,11 @@ const browserFS = {
delimiters: ['', ''],
'./fs/fs-impl': './build/fs-impl-browser'
}
const browserBase64 = {
include: './src/filters/base64.ts',
delimiters: ['', ''],
'./base64-impl': '../build/base64-impl-browser'
}
const browserStream = {
include: './src/emitters/index.ts',
delimiters: ['', ''],
@@ -94,6 +99,7 @@ const browserEsm = {
plugins: [
versionInjection,
replace(browserFS),
replace(browserBase64),
replace(browserStream),
typescript(tsconfig('es6'))
],
@@ -112,6 +118,7 @@ const browserUmd = {
plugins: [
versionInjection,
replace(browserFS),
replace(browserBase64),
replace(browserStream),
typescript(tsconfig('es5'))
],
@@ -130,6 +137,7 @@ const browserMin = {
plugins: [
versionInjection,
replace(browserFS),
replace(browserBase64),
replace(browserStream),
typescript(tsconfig('es5')),
uglify()
+101
View File
@@ -0,0 +1,101 @@
import * as base64 from './base64-impl-browser'
import { JSDOM } from 'jsdom'
describe('base64-impl/browser', function () {
if (+(process.version.match(/^v(\d+)/) as RegExpMatchArray)[1] < 8) {
console.info('jsdom not supported, skipping base64-impl-browser...')
return
}
beforeEach(function () {
const dom = new JSDOM(``, {
url: 'https://example.com/',
contentType: 'text/html',
includeNodeLocations: true
})
// Mock btoa and atob on global object
Object.defineProperty(global, 'btoa', {
value: dom.window.btoa,
writable: true,
configurable: true
})
Object.defineProperty(global, 'atob', {
value: dom.window.atob,
writable: true,
configurable: true
})
})
afterEach(function () {
delete (global as any).btoa
delete (global as any).atob
})
describe('#base64Encode()', function () {
it('should encode a simple string', function () {
expect(base64.base64Encode('one two three')).toBe('b25lIHR3byB0aHJlZQ==')
})
it('should encode an empty string', function () {
expect(base64.base64Encode('')).toBe('')
})
it('should encode a string with special characters', function () {
expect(base64.base64Encode('Hello, World! @#$%')).toBe('SGVsbG8sIFdvcmxkISBAIyQl')
})
it('should encode numeric strings', function () {
expect(base64.base64Encode('123')).toBe('MTIz')
})
it('should encode boolean strings', function () {
expect(base64.base64Encode('true')).toBe('dHJ1ZQ==')
})
})
describe('#base64Decode()', function () {
it('should decode a simple string', function () {
expect(base64.base64Decode('b25lIHR3byB0aHJlZQ==')).toBe('one two three')
})
it('should decode an empty string', function () {
expect(base64.base64Decode('')).toBe('')
})
it('should decode a string with special characters', function () {
expect(base64.base64Decode('SGVsbG8sIFdvcmxkISBAIyQl')).toBe('Hello, World! @#$%')
})
it('should decode numeric strings', function () {
expect(base64.base64Decode('MTIz')).toBe('123')
})
it('should decode boolean strings', function () {
expect(base64.base64Decode('dHJ1ZQ==')).toBe('true')
})
})
describe('round-trip encoding/decoding', function () {
it('should encode and decode back to original', function () {
const original = 'Hello, World!'
const encoded = base64.base64Encode(original)
const decoded = base64.base64Decode(encoded)
expect(decoded).toBe(original)
})
it('should handle complex strings with special characters', function () {
const original = 'Special chars: !@#$%^&*()_+-=[]{}|;:,.<>?'
const encoded = base64.base64Encode(original)
const decoded = base64.base64Decode(encoded)
expect(decoded).toBe(original)
})
it('should handle mixed unicode and ASCII', function () {
const original = 'Hello 🌍'
const encoded = base64.base64Encode(original)
const decoded = base64.base64Decode(encoded)
expect(decoded).toBe(original)
})
})
})
+10
View File
@@ -0,0 +1,10 @@
export function base64Encode (str: string): string {
return btoa(String.fromCharCode(...new TextEncoder().encode(str)))
}
export function base64Decode (str: string): string {
return new TextDecoder().decode(
Uint8Array.from(atob(str), c => c.charCodeAt(0))
)
}
+14 -14
View File
@@ -86,7 +86,7 @@ export class Context {
public * _getFromScope (scope: unknown, paths: (PropertyKey | Drop)[] | string, strictVariables = this.strictVariables): 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)
scope = yield this.readProperty(scope as object, paths[i])
if (strictVariables && isUndefined(scope)) {
throw new InternalUndefinedVariableError((paths as string[]).slice(0, i + 1).join!('.'))
}
@@ -120,21 +120,21 @@ export class Context {
if (key in this.environments) return this.environments
return this.globals
}
readProperty (obj: Scope, key: (PropertyKey | Drop)) {
obj = toLiquid(obj)
key = toValue(key) as PropertyKey
if (isNil(obj)) return obj
if (isArray(obj) && (key as number) < 0) return obj[obj.length + +key]
const value = readJSProperty(obj, key, this.ownPropertyOnly)
if (value === undefined && obj instanceof Drop) return obj.liquidMethodMissing(key, this)
if (isFunction(value)) return value.call(obj)
if (key === 'size') return readSize(obj)
else if (key === 'first') return readFirst(obj)
else if (key === 'last') return readLast(obj)
return value
}
}
export function readProperty (obj: Scope, key: (PropertyKey | Drop), ownPropertyOnly: boolean) {
obj = toLiquid(obj)
key = toValue(key) as PropertyKey
if (isNil(obj)) return obj
if (isArray(obj) && (key as number) < 0) return obj[obj.length + +key]
const value = readJSProperty(obj, key, ownPropertyOnly)
if (value === undefined && obj instanceof Drop) return obj.liquidMethodMissing(key)
if (isFunction(value)) return value.call(obj)
if (key === 'size') return readSize(obj)
else if (key === 'first') return readFirst(obj)
else if (key === 'last') return readLast(obj)
return value
}
export function readJSProperty (obj: Scope, key: PropertyKey, ownPropertyOnly: boolean) {
if (ownPropertyOnly && !hasOwnProperty.call(obj, key) && !(obj instanceof Drop)) return undefined
return obj[key]
+6 -3
View File
@@ -1,9 +1,10 @@
import { Emitter, SimpleEmitter } from '../emitters'
import { Drop } from './drop'
export class BlockDrop extends Drop {
constructor (
// the block render from layout template
private superBlockRender: () => Iterable<any> = () => ''
private superBlockRender: (emitter: Emitter) => IterableIterator<unknown> | string = () => ''
) {
super()
}
@@ -11,7 +12,9 @@ export class BlockDrop extends Drop {
* Provide parent access in child block by
* {{ block.super }}
*/
public super () {
return this.superBlockRender()
public * super (): IterableIterator<unknown> {
const emitter = new SimpleEmitter()
yield this.superBlockRender(emitter)
return emitter.buffer
}
}
+3 -1
View File
@@ -1,5 +1,7 @@
import { Context } from '../context'
export abstract class Drop {
public liquidMethodMissing (key: string | number): Promise<any> | any {
public liquidMethodMissing (key: string | number, context: Context): Promise<any> | any {
return undefined
}
}
+81 -23
View File
@@ -1,4 +1,4 @@
import { toArray, argumentsToValue, toValue, stringify, caseInsensitiveCompare, isArray, isNil, last as arrayLast, isArrayLike } from '../util'
import { toArray, argumentsToValue, toValue, stringify, caseInsensitiveCompare, isArray, isNil, last as arrayLast, isArrayLike, toEnumerable } from '../util'
import { arrayIncludes, equals, evalToken, isTruthy } from '../render'
import { Value, FilterImpl } from '../template'
import { Tokenizer } from '../parser'
@@ -117,7 +117,17 @@ export function slice<T> (this: FilterImpl, v: T[] | string, begin: number, leng
return v.slice(begin, begin + length)
}
export function * where<T extends object> (this: FilterImpl, arr: T[], property: string, expected?: any): IterableIterator<unknown> {
function expectedMatcher (this: FilterImpl, expected: any): (v: any) => boolean {
if (this.context.opts.jekyllWhere) {
return (v: any) => EmptyDrop.is(expected) ? equals(v, expected) : (isArray(v) ? arrayIncludes(v, expected) : equals(v, expected))
} else if (expected === undefined) {
return (v: any) => isTruthy(v, this.context)
} else {
return (v: any) => equals(v, expected)
}
}
function * filter<T extends object> (this: FilterImpl, include: boolean, arr: T[], property: string, expected: any): IterableIterator<unknown> {
const values: unknown[] = []
arr = toArray(arr)
this.context.memoryLimit.use(arr.length)
@@ -125,30 +135,43 @@ export function * where<T extends object> (this: FilterImpl, arr: T[], property:
for (const item of arr) {
values.push(yield evalToken(token, this.context.spawn(item)))
}
const matcher = this.context.opts.jekyllWhere
? (v: any) => EmptyDrop.is(expected) ? equals(v, expected) : (isArray(v) ? arrayIncludes(v, expected) : equals(v, expected))
: (v: any) => equals(v, expected)
return arr.filter((_, i) => {
if (expected === undefined) return isTruthy(values[i], this.context)
return matcher(values[i])
})
const matcher = expectedMatcher.call(this, expected)
return arr.filter((_, i) => matcher(values[i]) === include)
}
export function * where_exp<T extends object> (this: FilterImpl, arr: T[], itemName: string, exp: string): IterableIterator<unknown> {
function * filter_exp<T extends object> (this: FilterImpl, include: boolean, arr: T[], itemName: string, exp: string): IterableIterator<unknown> {
const filtered: unknown[] = []
const keyTemplate = new Value(stringify(exp), this.liquid)
const array = toArray(arr)
this.context.memoryLimit.use(array.length)
for (const item of array) {
const value = yield keyTemplate.value(this.context.spawn({ [itemName]: item }))
if (value) filtered.push(item)
this.context.push({ [itemName]: item })
const value = yield keyTemplate.value(this.context)
this.context.pop()
if (value === include) filtered.push(item)
}
return filtered
}
export function * where<T extends object> (this: FilterImpl, arr: T[], property: string, expected?: any): IterableIterator<unknown> {
return yield * filter.call(this, true, arr, property, expected)
}
export function * reject<T extends object> (this: FilterImpl, arr: T[], property: string, expected?: any): IterableIterator<unknown> {
return yield * filter.call(this, false, arr, property, expected)
}
export function * where_exp<T extends object> (this: FilterImpl, arr: T[], itemName: string, exp: string): IterableIterator<unknown> {
return yield * filter_exp.call(this, true, arr, itemName, exp)
}
export function * reject_exp<T extends object> (this: FilterImpl, arr: T[], itemName: string, exp: string): IterableIterator<unknown> {
return yield * filter_exp.call(this, false, arr, itemName, exp)
}
export function * group_by<T extends object> (this: FilterImpl, arr: T[], property: string): IterableIterator<unknown> {
const map = new Map()
arr = toArray(arr)
arr = toEnumerable(arr)
const token = new Tokenizer(stringify(property)).readScopeValue()
this.context.memoryLimit.use(arr.length)
for (const item of arr) {
@@ -162,34 +185,69 @@ export function * group_by<T extends object> (this: FilterImpl, arr: T[], proper
export function * group_by_exp<T extends object> (this: FilterImpl, arr: T[], itemName: string, exp: string): IterableIterator<unknown> {
const map = new Map()
const keyTemplate = new Value(stringify(exp), this.liquid)
arr = toArray(arr)
arr = toEnumerable(arr)
this.context.memoryLimit.use(arr.length)
for (const item of arr) {
const key = yield keyTemplate.value(this.context.spawn({ [itemName]: item }))
this.context.push({ [itemName]: item })
const key = yield keyTemplate.value(this.context)
this.context.pop()
if (!map.has(key)) map.set(key, [])
map.get(key).push(item)
}
return [...map.entries()].map(([name, items]) => ({ name, items }))
}
export function * find<T extends object> (this: FilterImpl, arr: T[], property: string, expected: string): IterableIterator<unknown> {
function * search<T extends object> (this: FilterImpl, arr: T[], property: string, expected: string): IterableIterator<unknown> {
const token = new Tokenizer(stringify(property)).readScopeValue()
const array = toArray(arr)
for (const item of array) {
const value = yield evalToken(token, this.context.spawn(item))
if (equals(value, expected)) return item
const matcher = expectedMatcher.call(this, expected)
for (let index = 0; index < array.length; index++) {
const value = yield evalToken(token, this.context.spawn(array[index]))
if (matcher(value)) return [index, array[index]]
}
}
export function * find_exp<T extends object> (this: FilterImpl, arr: T[], itemName: string, exp: string): IterableIterator<unknown> {
function * search_exp<T extends object> (this: FilterImpl, arr: T[], itemName: string, exp: string): IterableIterator<unknown> {
const predicate = new Value(stringify(exp), this.liquid)
const array = toArray(arr)
for (const item of array) {
const value = yield predicate.value(this.context.spawn({ [itemName]: item }))
if (value) return item
for (let index = 0; index < array.length; index++) {
this.context.push({ [itemName]: array[index] })
const value = yield predicate.value(this.context)
this.context.pop()
if (value) return [index, array[index]]
}
}
export function * has<T extends object> (this: FilterImpl, arr: T[], property: string, expected?: any): IterableIterator<unknown> {
const result = yield * search.call(this, arr, property, expected)
return !!result
}
export function * has_exp<T extends object> (this: FilterImpl, arr: T[], itemName: string, exp: string): IterableIterator<unknown> {
const result = yield * search_exp.call(this, arr, itemName, exp)
return !!result
}
export function * find_index<T extends object> (this: FilterImpl, arr: T[], property: string, expected?: any): IterableIterator<unknown> {
const result = yield * search.call(this, arr, property, expected)
return result ? result[0] : undefined
}
export function * find_index_exp<T extends object> (this: FilterImpl, arr: T[], itemName: string, exp: string): IterableIterator<unknown> {
const result = yield * search_exp.call(this, arr, itemName, exp)
return result ? result[0] : undefined
}
export function * find<T extends object> (this: FilterImpl, arr: T[], property: string, expected?: any): IterableIterator<unknown> {
const result = yield * search.call(this, arr, property, expected)
return result ? result[1] : undefined
}
export function * find_exp<T extends object> (this: FilterImpl, arr: T[], itemName: string, exp: string): IterableIterator<unknown> {
const result = yield * search_exp.call(this, arr, itemName, exp)
return result ? result[1] : undefined
}
export function uniq<T> (this: FilterImpl, arr: T[]): T[] {
arr = toArray(arr)
this.context.memoryLimit.use(arr.length)
+7
View File
@@ -0,0 +1,7 @@
export function base64Encode (str: string): string {
return Buffer.from(str, 'utf8').toString('base64')
}
export function base64Decode (str: string): string {
return Buffer.from(str, 'base64').toString('utf8')
}
+21
View File
@@ -0,0 +1,21 @@
/**
* Base64 related filters
*
* Implements base64_encode and base64_decode filters for Shopify compatibility
*/
import { FilterImpl } from '../template'
import { stringify } from '../util'
import { base64Encode, base64Decode } from './base64-impl'
export function base64_encode (this: FilterImpl, value: string): string {
const str = stringify(value)
this.context.memoryLimit.use(str.length)
return base64Encode(str)
}
export function base64_decode (this: FilterImpl, value: string): string {
const str = stringify(value)
this.context.memoryLimit.use(str.length)
return base64Decode(str)
}
+2
View File
@@ -4,6 +4,7 @@ import * as urlFilters from './url'
import * as arrayFilters from './array'
import * as dateFilters from './date'
import * as stringFilters from './string'
import * as base64Filters from './base64'
import misc from './misc'
import { FilterImplOptions } from '../template'
@@ -14,5 +15,6 @@ export const filters: Record<string, FilterImplOptions> = {
...arrayFilters,
...dateFilters,
...stringFilters,
...base64Filters,
...misc
}
+13 -18
View File
@@ -1,24 +1,19 @@
import { toValue, argumentsToValue } from '../util/underscore'
import { toNumber, argumentsToNumber } from '../util/underscore'
export const abs = argumentsToValue(Math.abs)
export const at_least = argumentsToValue(Math.max)
export const at_most = argumentsToValue(Math.min)
export const ceil = argumentsToValue(Math.ceil)
export const divided_by = argumentsToValue((dividend: number, divisor: number, integerArithmetic = false) => integerArithmetic ? Math.floor(dividend / divisor) : dividend / divisor)
export const floor = argumentsToValue(Math.floor)
export const minus = argumentsToValue((v: number, arg: number) => v - arg)
export const modulo = argumentsToValue((v: number, arg: number) => v % arg)
export const times = argumentsToValue((v: number, arg: number) => v * arg)
export const abs = argumentsToNumber(Math.abs)
export const at_least = argumentsToNumber(Math.max)
export const at_most = argumentsToNumber(Math.min)
export const ceil = argumentsToNumber(Math.ceil)
export const divided_by = argumentsToNumber((dividend: number, divisor: number, integerArithmetic = false) => integerArithmetic ? Math.floor(dividend / divisor) : dividend / divisor)
export const floor = argumentsToNumber(Math.floor)
export const minus = argumentsToNumber((v: number, arg: number) => v - arg)
export const plus = argumentsToNumber((lhs: number, rhs: number) => lhs + rhs)
export const modulo = argumentsToNumber((v: number, arg: number) => v % arg)
export const times = argumentsToNumber((v: number, arg: number) => v * arg)
export function round (v: number, arg = 0) {
v = toValue(v)
arg = toValue(arg)
v = toNumber(v)
arg = toNumber(arg)
const amp = Math.pow(10, arg)
return Math.round(v * amp) / amp
}
export function plus (v: number, arg: number) {
v = toValue(v)
arg = toValue(arg)
return Number(v) + Number(arg)
}
+7 -2
View File
@@ -2,15 +2,20 @@
export const version = '[VI]{version}[/VI]'
export * as TypeGuards from './util/type-guards'
export { toValue, createTrie, Trie, toPromise, toValueSync, assert, LiquidError, ParseError, RenderError, UndefinedVariableError, TokenizationError, AssertionError } from './util'
export type { LiquidErrors } from './util/error'
export { Drop } from './drop'
export type { Comparable } from './drop'
export { Emitter } from './emitters'
export { defaultOperators, Operators, evalToken, evalQuotedToken, Expression, isFalsy, isTruthy } from './render'
export { Context, Scope } from './context'
export { Value, Hash, Template, FilterImplOptions, Tag, Filter, Output, Variable, VariableLocation, VariableSegments, Variables, StaticAnalysis, StaticAnalysisOptions, analyze, analyzeSync, Arguments, PartialScope } from './template'
export type { TagRenderReturn } from './template'
export { Token, TopLevelToken, TagToken, ValueToken } from './tokens'
export type { RangeToken, LiteralToken, QuotedToken, PropertyAccessToken, NumberToken } from './tokens'
export { TokenKind, Tokenizer, ParseStream, Parser } from './parser'
export { filters } from './filters'
export * from './tags'
export { defaultOptions, LiquidOptions } from './liquid-options'
export { FS } from './fs'
export { defaultOptions } from './liquid-options'
export type { LiquidOptions, RenderOptions, RenderFileOptions } from './liquid-options'
export { FS, LookupType } from './fs'
export { Liquid } from './liquid'
+5 -3
View File
@@ -391,9 +391,11 @@ export class Tokenizer {
if (this.peek() !== '(') return
++this.p
const lhs = this.readValueOrThrow()
this.p += 2
this.skipBlank()
this.assert(this.read() === '.' && this.read() === '.', 'invalid range syntax')
const rhs = this.readValueOrThrow()
++this.p
this.skipBlank()
this.assert(this.read() === ')', 'invalid range syntax')
return new RangeToken(this.input, begin, this.p, lhs, rhs, this.file)
}
@@ -420,7 +422,7 @@ export class Tokenizer {
* readFileNameTemplate (options: NormalizedFullOptions): IterableIterator<TopLevelToken> {
const { outputDelimiterLeft } = options
const htmlStopStrings = [',', ' ', outputDelimiterLeft]
const htmlStopStrings = [',', ' ', '\r', '\n', '\t', outputDelimiterLeft]
const htmlStopStringSet = new Set(htmlStopStrings)
// break on ',' and ' ', outputDelimiterLeft only stops HTML token
while (this.p < this.N && !htmlStopStringSet.has(this.peek())) {
+24 -5
View File
@@ -24,11 +24,6 @@ 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 eval range expression', async function () {
const ctx = new Context({ two: 2 })
expect(await toPromise(create('(2..4)').evaluate(ctx, false))).toEqual([2, 3, 4])
expect(await toPromise(create('(two..4)').evaluate(ctx, false))).toEqual([2, 3, 4])
})
it('should eval literal', async function () {
expect(await toPromise(create('2.4').evaluate(ctx, false))).toBe(2.4)
expect(await toPromise(create('"foo"').evaluate(ctx, false))).toBe('foo')
@@ -221,6 +216,30 @@ describe('Expression', function () {
})
})
describe('range', function () {
const ctx = new Context({ two: 2, num: { one: 1, two: 2 } })
it('should eval range expression', async function () {
expect(await toPromise(create('(2..4)').evaluate(ctx, false))).toEqual([2, 3, 4])
expect(await toPromise(create('(two..4)').evaluate(ctx, false))).toEqual([2, 3, 4])
})
it('should allow property access expression as variables', async function () {
expect(await toPromise(create('(num.one..num.two)').evaluate(ctx))).toEqual([1, 2])
expect(await toPromise(create('(num.one .. two)').evaluate(ctx))).toEqual([1, 2])
})
it('should allow blanks in range', async function () {
expect(await toPromise(create('(3 ..5)').evaluate(ctx))).toEqual([3, 4, 5])
expect(await toPromise(create('(3 .. 5)').evaluate(ctx))).toEqual([3, 4, 5])
expect(await toPromise(create('( 3 .. 5 )').evaluate(ctx))).toEqual([3, 4, 5])
})
it('should throw if .. not matched', async function () {
expect(() => create('(3.5')).toThrow('invalid range syntax')
expect(() => create('(3 5')).toThrow('invalid range syntax')
})
it('should throw if ( not patched', async function () {
expect(() => create('(3..5')).toThrow('invalid range syntax')
})
})
describe('sync', function () {
it('should eval literal', function () {
expect(toValueSync(create('2.4').evaluate(ctx, false))).toBe(2.4)
+5 -1
View File
@@ -39,7 +39,11 @@ export default class extends Tag {
ctx.pop()
}
return renderChild
? (superBlock: BlockDrop, emitter: Emitter) => renderChild(new BlockDrop(() => renderCurrent(superBlock, emitter)), emitter)
? (superBlock: BlockDrop, emitter: Emitter) => renderChild(
new BlockDrop(
(emitter: Emitter) => renderCurrent(superBlock, emitter)
),
emitter)
: renderCurrent
}
+1
View File
@@ -74,6 +74,7 @@ export default class extends Tag {
if (ctx.breakCalled) break
scope.forloop.next()
}
ctx.continueCalled = ctx.breakCalled = false
ctx.pop()
}
+2 -4
View File
@@ -5,6 +5,7 @@ import type { NormalizedFullOptions } from '../liquid-options'
export class TagToken extends DelimitedToken {
public name: string
public tokenizer: Tokenizer
public readonly args: string;
public constructor (
input: string,
begin: number,
@@ -20,9 +21,6 @@ export class TagToken extends DelimitedToken {
this.name = this.tokenizer.readTagName()
this.tokenizer.assert(this.name, `illegal tag syntax, tag name expected`)
this.tokenizer.skipBlank()
}
get args (): string {
return this.tokenizer.input.slice(this.tokenizer.p, this.contentRange[1])
this.args = this.tokenizer.input.slice(this.tokenizer.p, this.contentRange[1])
}
}
+2 -3
View File
@@ -1,5 +1,4 @@
import { assert } from './assert'
import { toNumber } from './underscore'
export class Limiter {
private message: string
@@ -10,12 +9,12 @@ export class Limiter {
this.limit = limit
}
use (count: number) {
count = toNumber(count)
count = +count || 0
assert(this.base + count <= this.limit, this.message)
this.base += count
}
check (count: number) {
count = toNumber(count)
count = +count || 0
assert(count <= this.limit, this.message)
}
}
+6 -2
View File
@@ -3,7 +3,11 @@ import { isString } from './underscore'
// one minute in milliseconds
const OneMinute = 60000
const ISO8601_TIMEZONE_PATTERN = /([zZ]|([+-])(\d{2}):(\d{2}))$/
/**
* Need support both ISO8601 and RFC2822 as in major browsers & NodeJS
* RFC2822: https://datatracker.ietf.org/doc/html/rfc2822#section-3.3
*/
const TIMEZONE_PATTERN = /([zZ]|([+-])(\d{2}):?(\d{2}))$/
const monthNames = [
'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August',
'September', 'October', 'November', 'December'
@@ -126,7 +130,7 @@ export class LiquidDate {
* Given that a template is expected to be parsed fewer times than rendered.
*/
static createDateFixedToTimezone (dateString: string, locale: string): LiquidDate {
const m = dateString.match(ISO8601_TIMEZONE_PATTERN)
const m = dateString.match(TIMEZONE_PATTERN)
// representing a UTC timestamp
if (m && m[1] === 'Z') {
return new LiquidDate(+new Date(dateString), locale, 0)
+5 -2
View File
@@ -67,8 +67,7 @@ export function toValue (value: any): any {
}
export function toNumber (value: any): number {
value = Number(value)
return isNaN(value) ? 0 : value
return +toValue(value) || 0
}
export function isNumber (value: any): value is number {
@@ -191,6 +190,10 @@ export function argumentsToValue<F extends (...args: any) => any, T> (fn: F) {
return function (this: T, ...args: Parameters<F>) { return fn.call(this, ...args.map(toValue)) }
}
export function argumentsToNumber<F extends (...args: any) => any, T> (fn: F) {
return function (this: T, ...args: Parameters<F>) { return fn.call(this, ...args.map(toNumber)) }
}
export function escapeRegExp (text: string) {
return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')
}
+28 -16
View File
@@ -1,25 +1,37 @@
import { Drop, Liquid } from '../..'
class SettingsDrop extends Drop {
public foo = 'FOO'
public bar () {
return 'BAR'
}
public liquidMethodMissing (key: string) {
return key.toUpperCase()
}
}
import { Context, Drop, Liquid } from '../..'
describe('drop', function () {
const settings = new SettingsDrop()
let engine: Liquid
beforeEach(function () {
engine = new Liquid()
})
it('should support liquidMethodMissing', async function () {
const src = `{{settings.foo}},{{settings.bar}},{{settings.coo}}`
const html = await engine.parseAndRender(src, { settings })
return expect(html).toBe('FOO,BAR,COO')
describe('liquidMethodMissing', () => {
it('should support liquidMethodMissing', async function () {
class SettingsDrop extends Drop {
public foo = 'FOO'
public bar () {
return 'BAR'
}
public liquidMethodMissing (key: string) {
return key.toUpperCase()
}
}
const src = `{{settings.foo}},{{settings.bar}},{{settings.coo}}`
const html = await engine.parseAndRender(src, { settings: new SettingsDrop() })
return expect(html).toBe('FOO,BAR,COO')
})
it('should expose context', async function () {
class SettingsDrop extends Drop {
public liquidMethodMissing (key: string, context: Context) {
return key + ':' + context.getSync([key])
}
}
const src = `{{settings.foo}}`
const html = await engine.parseAndRender(src, { settings: new SettingsDrop(), foo: 'FOO' })
return expect(html).toBe('foo:FOO')
})
})
describe('BlandDrop', function () {
+35 -1
View File
@@ -1,4 +1,4 @@
import { TopLevelToken, TagToken, Tokenizer, Context, Liquid, Drop, toValueSync, LiquidError } from '../..'
import { TopLevelToken, TagToken, Tokenizer, Context, Liquid, Drop, toValueSync, LiquidError, IfTag } from '../..'
const LiquidUMD = require('../../dist/liquid.browser.umd.js').Liquid
describe('Issues', function () {
@@ -531,4 +531,38 @@ describe('Issues', function () {
const tpl = `{% for i in (1..1000000000) %} {{'a'}} {% endfor %}`
expect(() => engine.parseAndRenderSync(tpl)).toThrow('memory alloc limit exceeded, line:1, col:1')
})
it('group_by_exp fails with object as input #785', () => {
const site = {
tags: {
CPP: [ 'page0' ],
PHP: [ 'page0', 'page2' ],
JavaScript: [ 'page1', 'page2', 'page3' ],
CSharp: [ 'page2', 'page4' ]
}
}
const tpl = `
{%- assign tags_by_size = site.tags | group_by_exp: 'tag', 'tag[1].size' | sort: 'name' | reverse -%}
{%- for tags_with_size in tags_by_size -%}
{%- for tag in tags_with_size.items -%}
{%- assign tag_name = tag[0] %}
{{ tag_name }} <sup>{{ tags_with_size.name }}</sup> Posts:
{%- for post in tag[1] -%}{{post}},{%- endfor -%}
{%- endfor -%}
{%- endfor -%}
`
const engine = new Liquid()
const html = engine.parseAndRenderSync(tpl, { site })
expect(html).toEqual(`
JavaScript <sup>3</sup> Posts:page1,page2,page3,
PHP <sup>2</sup> Posts:page0,page2,
CSharp <sup>2</sup> Posts:page2,page4,
CPP <sup>1</sup> Posts:page0,`)
})
it('if tag condition, the token args is empty #796', () => {
const tpl = 'Hello, {% if name %} {{ name }} {% else %} user {% endif %}'
const engine = new Liquid()
const parsed = engine.parse(tpl)
const ifToken = parsed[1]
expect((ifToken as IfTag).token.args).toEqual('name')
})
})
+302 -2
View File
@@ -417,6 +417,32 @@ describe('filters/array', function () {
- Boring sneakers
`)
})
it('should support filter with undefined target', function () {
return test(`{% assign typed_products = products | where: "type", notdefined %}
Typed products:
{% for product in typed_products -%}
- {{ product.title }}
{% endfor %}`, { products }, `
Typed products:
- Vacuum
- Spatula
- Television
- Garlic press
`)
})
it('should support no target', function () {
return test(`{% assign typed_products = products | where: "type" %}
Typed products:
{% for product in typed_products -%}
- {{ product.title }}
{% endfor %}`, { products }, `
Typed products:
- Vacuum
- Spatula
- Television
- Garlic press
`)
})
it('should support nested property', async function () {
const authors = [
{ name: 'Alice', books: { year: 2019 } },
@@ -491,7 +517,7 @@ describe('filters/array', function () {
await test('{{objs | where: "foo", "FOO" | json}}', scope, '[]')
})
describe('jekyll style', () => {
it('should not match string with array', async () => {
it('should filter arrays by inclusion', async () => {
const scope = { objs: [{ foo: ['FOO', 'bar'] }] }
await test('{{objs | where: "foo", "FOO" | json}}', scope, '[{"foo":["FOO","bar"]}]', { jekyllWhere: true })
})
@@ -499,6 +525,14 @@ describe('filters/array', function () {
const scope = { pages: [{ tags: ['FOO'] }, { tags: [] }, { title: 'foo' }] }
await test('{{pages | where: "tags", empty | json}}', scope, '[{"tags":[]}]', { jekyllWhere: true })
})
it('should filter by undefined when target is omitted', async () => {
await test('{{products | where: "type" | map: "title" | join: ","}}', { products },
'Coffee mug,Limited edition sneakers,Boring sneakers', { jekyllWhere: true })
})
it('should filter plainly when target is undefined', async () => {
await test('{{products | where: "type", notdefined | map: "title" | join: ","}}', { products },
'Coffee mug,Limited edition sneakers,Boring sneakers', { jekyllWhere: true })
})
})
})
describe('where_exp', function () {
@@ -522,6 +556,103 @@ describe('filters/array', function () {
- Garlic press
`)
})
it('should be aware context', function () {
const tpl = `{% assign kitchen_products = products | where_exp: "item", "item.type == target" %}
Kitchen products:
{% for product in kitchen_products -%}
- {{ product.title }}
{% endfor %}`
const scope = { products, target: 'kitchen' }
const html = `
Kitchen products:
- Spatula
- Garlic press
`
return test(tpl, scope, html)
})
})
describe('reject', function () {
const products = [
{ title: 'Vacuum', type: 'living room' },
{ title: 'Spatula', type: 'kitchen' },
{ title: 'Television', type: 'living room' },
{ title: 'Garlic press', type: 'kitchen' },
{ title: 'Coffee mug', available: true },
{ title: 'Limited edition sneakers', available: false },
{ title: 'Boring sneakers', available: true }
]
it('should support reject by property value', function () {
return test(`{% assign kitchen_products = products | reject: "type", "kitchen" %}
Kitchen products:
{% for product in kitchen_products -%}
- {{ product.title }}
{% endfor %}`, { products }, `
Kitchen products:
- Vacuum
- Television
- Coffee mug
- Limited edition sneakers
- Boring sneakers
`)
})
it('should support reject truthy property', function () {
return test(`{% assign unavailable_products = products | reject: "available" %}
Unavailable products:
{% for product in unavailable_products -%}
- {{ product.title }}
{% endfor %}`, { products }, `
Unavailable products:
- Vacuum
- Spatula
- Television
- Garlic press
- Limited edition sneakers
`)
})
it('should support reject by string property', function () {
return test(`{% assign untyped_products = products | reject: "type" %}
Untyped products:
{% for product in untyped_products -%}
- {{ product.title }}
{% endfor %}`, { products }, `
Untyped products:
- Coffee mug
- Limited edition sneakers
- Boring sneakers
`)
})
describe('jekyll style', () => {
it('should filter arrays by exclusion', async () => {
const scope = { objs: [{ foo: ['FOO', 'bar'] }, { foo: ['bar', 'baz'] }, { foo: ['FOO'] }] }
await test('{{objs | reject: "foo", "FOO" | json}}', scope, '[{"foo":["bar","baz"]}]', { jekyllWhere: true })
})
it('should filter by undefined when target is omitted', async () => {
await test('{{products | reject: "type" | map: "title" | join: ","}}', { products },
'Vacuum,Spatula,Television,Garlic press', { jekyllWhere: true })
})
})
})
describe('reject_exp', function () {
const products = [
{ title: 'Vacuum', type: 'living room' },
{ title: 'Spatula', type: 'kitchen' },
{ title: 'Television', type: 'living room' },
{ title: 'Garlic press', type: 'kitchen' },
{ title: 'Coffee mug', available: true },
{ title: 'Limited edition sneakers', available: false },
{ title: 'Boring sneakers', available: true }
]
it('should support reject by exp', function () {
return test(`{% assign kitchen_products = products | reject_exp: "item", "item.type != 'kitchen'" %}
Kitchen products:
{% for product in kitchen_products -%}
- {{ product.title }}
{% endfor %}`, { products }, `
Kitchen products:
- Spatula
- Garlic press
`)
})
})
describe('group_by', function () {
const members = [
@@ -554,6 +685,12 @@ describe('filters/array', function () {
{ graduation_year: 2014, name: 'John' },
{ graduation_year: 2009, name: 'Jack' }
]
const postsByTags = {
CPP: [ 'page0' ],
PHP: [ 'page0', 'page2' ],
JavaScript: [ 'page1', 'page2', 'page3' ],
CSharp: [ 'page2', 'page4' ]
}
it('should support group by expression', function () {
const expected = [{
name: '201',
@@ -572,13 +709,100 @@ describe('filters/array', function () {
{ members },
JSON.stringify(expected))
})
it('should group key/values in plain object', function () {
const expected = [{
name: 3,
items: [
['JavaScript', ['page1', 'page2', 'page3']]
]
}, {
name: 2,
items: [
['PHP', ['page0', 'page2']],
['CSharp', ['page2', 'page4']]
]
}, {
name: 1,
items: [
['CPP', ['page0']]
]
}]
return test(
`{{ postsByTags | group_by_exp: "tag", "tag[1].size" | sort: 'name' | reverse | json}}`,
{ postsByTags },
JSON.stringify(expected))
})
})
describe('find', function () {
describe('has', function () {
const members = [
{ graduation_year: 2013, name: 'Jay' },
{ graduation_year: 2014, name: 'John' },
{ graduation_year: 2014, name: 'Jack', age: 13 }
]
it('should support has with no value', function () {
return test(
`{{ members | has: "age" | json }}, {{ members | has: "height" | json }}`,
{ members },
`true, false`)
})
it('should support has by property', function () {
return test(
`{{ members | has: "graduation_year", 2014 | json }}`,
{ members },
`true`)
})
it('should return false if not found', function () {
return test(
`{{ members | has: "graduation_year", 2018 | json }}`,
{ members },
`false`)
})
describe('jekyll style', () => {
it('should select array by inclusion', async () => {
const scope = { objs: [{ foo: ['FOO', 'bar'] }] }
await test('{{objs | has: "foo", "FOO" | json}}', scope, 'true', { jekyllWhere: true })
})
it('should support empty as target', async () => {
const scope = { pages: [{ tags: ['FOO'] }, { tags: [] }, { title: 'foo' }] }
await test('{{pages | has: "tags", empty | json}}', scope, 'true', { jekyllWhere: true })
})
it('should search plainly when target is undefined', async () => {
await test('{{members | has: "age", notdefined}}', { members }, 'true', { jekyllWhere: true })
await test('{{members | has: "name", notdefined}}', { members }, 'false', { jekyllWhere: true })
})
})
})
describe('has_exp', function () {
const members = [
{ graduation_year: 2013, name: 'Jay' },
{ graduation_year: 2014, name: 'John' },
{ graduation_year: 2014, name: 'Jack' }
]
it('should support has by expression', function () {
return test(
`{{ members | has_exp: "item", "item.graduation_year == 2014" | json }}`,
{ members },
`true`)
})
it('should return false if not found', function () {
return test(
`{{ members | has_exp: "item", "item.graduation_year == 2018" | json }}`,
{ members },
`false`)
})
})
describe('find', function () {
const members = [
{ graduation_year: 2013, name: 'Jay' },
{ graduation_year: 2014, name: 'John' },
{ graduation_year: 2014, name: 'Jack', age: 13 }
]
it('should support find with no value', function () {
return test(
`{{ members | find: "age" | json }}`,
{ members },
`{"graduation_year":2014,"name":"Jack","age":13}`)
})
it('should support find by property', function () {
return test(
`{{ members | find: "graduation_year", 2014 | json }}`,
@@ -591,6 +815,24 @@ describe('filters/array', function () {
{ members },
``)
})
describe('jekyll style', () => {
it('should select array by inclusion', async () => {
const scope = { objs: [{ foo: ['FOO', 'bar'] }] }
await test('{{objs | find: "foo", "FOO" | json}}', scope, '{"foo":["FOO","bar"]}', { jekyllWhere: true })
})
it('should support empty as target', async () => {
const scope = { pages: [{ tags: ['FOO'] }, { tags: [] }, { title: 'foo' }] }
await test('{{pages | find: "tags", empty | json}}', scope, '{"tags":[]}', { jekyllWhere: true })
})
it('should search plainly when target is undefined', async () => {
await test(
'{{members | find: "age", notdefined | json}}',
{ members },
'{"graduation_year":2013,"name":"Jay"}',
{ jekyllWhere: true })
await test('{{members | find: "name", notdefined | json}}', { members }, '', { jekyllWhere: true })
})
})
})
describe('find_exp', function () {
const members = [
@@ -611,4 +853,62 @@ describe('filters/array', function () {
``)
})
})
describe('find_index', function () {
const members = [
{ graduation_year: 2013, name: 'Jay' },
{ graduation_year: 2014, name: 'John' },
{ graduation_year: 2014, name: 'Jack', age: 13 }
]
it('should support find_index with no value', function () {
return test(
`{{ members | find_index: "age" | json }}`,
{ members },
`2`)
})
it('should support find_index by property', function () {
return test(
`{{ members | find_index: "graduation_year", 2014 | json }}`,
{ members },
`1`)
})
it('should render none if not found', function () {
return test(
`{{ members | find_index: "graduation_year", 2018 | json }}`,
{ members },
``)
})
describe('jekyll style', () => {
it('should select array by inclusion', async () => {
const scope = { objs: [{ foo: ['FOO', 'bar'] }] }
await test('{{objs | find_index: "foo", "FOO" | json}}', scope, '0', { jekyllWhere: true })
})
it('should support empty as target', async () => {
const scope = { pages: [{ tags: ['FOO'] }, { tags: [] }, { title: 'foo' }] }
await test('{{pages | find_index: "tags", empty | json}}', scope, '1', { jekyllWhere: true })
})
it('should search plainly when target is undefined', async () => {
await test('{{members | find_index: "age", notdefined | json}}', { members }, '0', { jekyllWhere: true })
await test('{{members | find_index: "name", notdefined | json}}', { members }, '', { jekyllWhere: true })
})
})
})
describe('find_index_exp', function () {
const members = [
{ graduation_year: 2013, name: 'Jay' },
{ graduation_year: 2014, name: 'John' },
{ graduation_year: 2014, name: 'Jack' }
]
it('should support find_index by expression', function () {
return test(
`{{ members | find_index_exp: "item", "item.graduation_year == 2014" | json }}`,
{ members },
`1`)
})
it('should render none if not found', function () {
return test(
`{{ members | find_index_exp: "item", "item.graduation_year == 2018" | json }}`,
{ members },
``)
})
})
})
+78
View File
@@ -0,0 +1,78 @@
import { test } from '../../stub/render'
describe('filters/base64', function () {
describe('base64_encode', function () {
it('should encode a simple string', () => {
return test('{{ "one two three" | base64_encode }}', 'b25lIHR3byB0aHJlZQ==')
})
it('should encode an empty string', () => {
return test('{{ "" | base64_encode }}', '')
})
it('should encode a string with special characters', () => {
return test('{{ "Hello, World! @#$%" | base64_encode }}', 'SGVsbG8sIFdvcmxkISBAIyQl')
})
it('should encode unicode characters', () => {
return test('{{ "你好世界" | base64_encode }}', '5L2g5aW95LiW55WM')
})
it('should handle undefined input', () => {
return test('{{ foo | base64_encode }}', '')
})
it('should handle null input', () => {
return test('{{ null | base64_encode }}', '')
})
it('should handle numeric input', () => {
return test('{{ 123 | base64_encode }}', 'MTIz')
})
it('should handle boolean input', () => {
return test('{{ true | base64_encode }}', 'dHJ1ZQ==')
})
})
describe('base64_decode', function () {
it('should decode a simple string', () => {
return test('{{ "b25lIHR3byB0aHJlZQ==" | base64_decode }}', 'one two three')
})
it('should decode an empty string', () => {
return test('{{ "" | base64_decode }}', '')
})
it('should decode a string with special characters', () => {
return test('{{ "SGVsbG8sIFdvcmxkISBAIyQl" | base64_decode }}', 'Hello, World! @#$%')
})
it('should handle undefined input', () => {
return test('{{ foo | base64_decode }}', '')
})
it('should handle null input', () => {
return test('{{ null | base64_decode }}', '')
})
it('should handle numeric input', () => {
return test('{{ "MTIz" | base64_decode }}', '123')
})
it('should handle boolean input', () => {
return test('{{ "dHJ1ZQ==" | base64_decode }}', 'true')
})
})
describe('base64 round-trip', function () {
it('should encode and decode back to original', () => {
return test('{{ "Hello, World!" | base64_encode | base64_decode }}', 'Hello, World!')
})
it('should handle complex strings', () => {
const complexString = 'Special chars: !@#$%^&*()_+-=[]{}|;:,.<>?'
return test(`{{ "${complexString}" | base64_encode | base64_decode }}`, complexString)
})
})
})
+8 -4
View File
@@ -80,19 +80,23 @@ describe('filters/date', function () {
return test('{{ "1991-02-22T00:00:00" | date: "%Y-%m-%dT%H:%M:%S"}}', '1991-02-22T00:00:00')
})
describe('when preserveTimezones is enabled', function () {
const opts: LiquidOptions = { preserveTimezones: true }
const opts: LiquidOptions = { preserveTimezones: true, locale: 'en-US' }
it('should not change the timezone between input and output', function () {
return test('{{ "1990-12-31T23:00:00Z" | date: "%Y-%m-%dT%H:%M:%S"}}', '1990-12-31T23:00:00', undefined, opts)
})
it('should apply numeric timezone offset (0)', function () {
return test('{{ "1990-12-31T23:00:00+00:00" | date: "%Y-%m-%dT%H:%M:%S"}}', '1990-12-31T23:00:00', undefined, opts)
return test('{{ "1990-12-31T23:00:00+00:00" | date: "%Y-%m-%dT%H:%M:%S %z"}}', '1990-12-31T23:00:00 +0000', undefined, opts)
})
it('should apply numeric timezone offset (-1)', function () {
return test('{{ "1990-12-31T23:00:00-01:00" | date: "%Y-%m-%dT%H:%M:%S"}}', '1990-12-31T23:00:00', undefined, opts)
return test('{{ "1990-12-31T23:00:00-01:00" | date: "%Y-%m-%dT%H:%M:%S %z"}}', '1990-12-31T23:00:00 -0100', undefined, opts)
})
it('should apply numeric timezone offset (+2.30)', function () {
return test('{{ "1990-12-31T23:00:00+02:30" | date: "%Y-%m-%dT%H:%M:%S"}}', '1990-12-31T23:00:00', undefined, opts)
return test('{{ "1990-12-31T23:00:00+02:30" | date: "%Y-%m-%dT%H:%M:%S %z"}}', '1990-12-31T23:00:00 +0230', undefined, opts)
})
it('should support timezone in more casual JavaScript Date', async () => {
await test('{{ "2025-01-02 03:04:05 -0100" | date: "%Y-%m-%dT%H:%M:%S %z" }}', '2025-01-02T03:04:05 -0100', undefined, opts)
await test('{{ "2025-01-02 03:04:05 -0100" | date }}', 'Thursday, January 2, 2025 at 3:04 am -0100', undefined, opts)
})
it('should automatically work when timezone not specified', function () {
return test('{{ "1990-12-31T23:00:00" | date: "%Y-%m-%dT%H:%M:%S"}}', '1990-12-31T23:00:00', undefined, opts)
+3 -2
View File
@@ -56,8 +56,9 @@ describe('filters/math', function () {
it('should return "20" for 16,4', () => test('{{ 16 | plus: 4 }}', '20'))
it('should return "195.357" for 183.357,12',
() => test('{{ 183.357 | plus: 12 }}', '195.357'))
it('should convert first arg as number', () => test('{{ "4" | plus: 2 }}', '6'))
it('should convert both args as number', () => test('{{ "4" | plus: "2" }}', '6'))
it('should convert first arg to number', () => test('{{ "4" | plus: 2 }}', '6'))
it('should convert both args to numbers', () => test('{{ "4" | plus: "2" }}', '6'))
it('should convert invalid string to 0', () => test('{{ "abc" | plus: "2" }}', '2'))
it('should support variable', () => test('{{ 4 | plus: b }}', { b: 2 }, '6'))
})
+34
View File
@@ -178,6 +178,19 @@ describe('tags/for', function () {
const html = await liquid.parseAndRender(src, scope)
return expect(html).toBe(' before before')
})
it('should continue current forloop only', async () => {
const src = `
{%- for i in (1..2) -%}
i:{{ i }},
{%- for j in (1..2) -%}
j:{{ j }},
{%- continue -%}
after
{%- endfor -%}
{%- endfor -%}`
const html = await liquid.parseAndRender(src, scope)
return expect(html).toBe('i:1,j:1,j:2,i:2,j:1,j:2,')
})
})
describe('break', function () {
it('should support break', async function () {
@@ -196,6 +209,27 @@ describe('tags/for', function () {
const html = await liquid.parseAndRender(src, scope)
return expect(html).toBe('123breaking')
})
it('should not break template outside of forloop', async () => {
const src = '{% for i in (1..5) %}' +
'{{ i }}' +
'{% break %}' +
'{% endfor %}' +
' after'
const html = await liquid.parseAndRender(src, scope)
return expect(html).toBe('1 after')
})
it('should not break parent forloop', async function () {
const src = `
{%- for i in (1..3) -%}
i:{{ i }},
{%- for j in (1..3) -%}
j:{{ j }},
{%- break -%}
{%- endfor -%}
{%- endfor -%}`
const html = await liquid.parseAndRender(src, scope)
return expect(html).toBe('i:1,j:1,i:2,j:1,i:3,j:1,')
})
})
describe('limit', function () {
+8
View File
@@ -265,6 +265,14 @@ describe('tags/include', function () {
const html = liquid.renderFileSync('/current.html')
return expect(html).toBe('FOO-')
})
it('should support Jekyll style include with other whitespace before filename', function () {
mock({
'/current.html': '{% include bar/foo.html\r\n\ntitle="TITLE"\tcontent="FOO" %}',
'/bar/foo.html': '{{include.title}}={{include.content}}-{{content}}'
})
const html = liquid.renderFileSync('/current.html')
return expect(html).toBe('TITLE=FOO-')
})
it('should support multiple parameters', function () {
mock({
'/current.html': '{% include bar/foo.html header="HEADER" content="CONTENT" %}',
+20
View File
@@ -101,6 +101,26 @@ describe('tags/layout', function () {
const output = '<link href="base.css" rel="stylesheet"><link href="extra.css" rel="stylesheet">'
return expect(html).toBe(output)
})
it('should pass block.super as a string', async function () {
mock({
'/parent.html': '{% block css %}<link href="base.css" rel="stylesheet">{% endblock %}'
})
const src = '{% layout "parent.html" %}' +
'{%block css%}{{block.super}}<meta basesize={{block.super | size}}>{%endblock%}'
const html = await liquid.parseAndRender(src)
const output = '<link href="base.css" rel="stylesheet"><meta basesize=39>'
return expect(html).toBe(output)
})
it('should support block.super while strictVariables', async function () {
mock({
'/parent.html': '{% block css %}<link href="base.css" rel="stylesheet">{% endblock %}'
})
const src = '{% layout "parent.html" %}' +
'{%block css%}{{block.super}}<link href="extra.css" rel="stylesheet">{%endblock%}'
const html = await liquid.parseAndRender(src, undefined, { strictVariables: true })
const output = '<link href="base.css" rel="stylesheet"><link href="extra.css" rel="stylesheet">'
return expect(html).toBe(output)
})
it('should render block.super to empty if no parent exists', async function () {
mock({
'/parent.html': '{% block css %}{{block.super}}<link href="base.css" rel="stylesheet">{% endblock %}'