Compare commits

...
37 Commits
Author SHA1 Message Date
Harttle 40bc417cb6 chore: update semantic-release dependencies 2026-03-08 03:59:01 +08:00
Yang JunandGitHub eb4683ee3f chore: update to NPM Trusted Release (#860) 2026-03-08 03:35:49 +08:00
Yang JunandGitHub f1fc573a65 docs: state differences regarding inspect array/hash, #852, #853 (#858) 2026-03-08 03:14:13 +08:00
allcontributors[bot]GitHuballcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
524cd92cfe docs: add peaktwilight as a contributor for code (#857)
* docs: update README.md [skip ci]

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

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2026-03-08 02:40:16 +08:00
allcontributors[bot]GitHuballcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
0d9e797889 docs: add MorielHarush as a contributor for code (#856)
* docs: update README.md [skip ci]

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

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2026-03-08 02:39:45 +08:00
3cd024d652 fix: path traversal vulnerability, #851 (#855)
* Fix Path Traversal fallback

* Update loader.ts

Fixed nested

* Update loader.ts

padding fix

* refactor: reuse root enforcing

* docs: update test case and docs

---------

Co-authored-by: MorielHarush <[email protected]>
2026-03-08 02:36:09 +08:00
Yang JunandGitHub 85233e0568 docs: update testmu sponsor link (#850) 2026-02-14 13:54:03 +08:00
Yang JunandGitHub 02403a1879 docs: Change LambdaTest to TestMu AI (#848) 2026-01-19 23:26:49 +08:00
Yang JunandGitHub 1c6316111d docs: update docs for operators (#847) 2026-01-10 22:09:10 +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
53 changed files with 10924 additions and 5373 deletions
+81
View File
@@ -721,6 +721,87 @@
"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"
]
},
{
"login": "MorielHarush",
"name": "MorielHarush",
"avatar_url": "https://avatars.githubusercontent.com/u/93482738?v=4",
"profile": "https://github.com/MorielHarush",
"contributions": [
"code"
]
},
{
"login": "peaktwilight",
"name": "Peak Twilight",
"avatar_url": "https://avatars.githubusercontent.com/u/77903714?v=4",
"profile": "https://doruk.ch",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,
+6 -1
View File
@@ -4,6 +4,11 @@ jobs:
release:
name: Release
runs-on: ubuntu-latest
permissions:
contents: write
issues: write
pull-requests: write
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v3
@@ -12,7 +17,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
+7 -7
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
@@ -44,7 +44,7 @@ jobs:
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-npm-logs-${{ matrix.os }}-${{ matrix.timezone }}-${{ matrix.node-version }}
name: test-npm-logs-${{ matrix.os }}-${{ matrix.node-version }}
path: ~/.npm/_logs
demo:
name: Demo Check
+33
View File
@@ -1,3 +1,36 @@
# [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)
+36 -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.testmuai.com/?utm_medium=sponsor&utm_source=liquidjs" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://avatars.githubusercontent.com/u/27130435?s=200&v=4" width="80" style="vertical-align: middle;" alt="TestMu AI" title="TestMu AI"/></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,17 @@ 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>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MorielHarush"><img src="https://avatars.githubusercontent.com/u/93482738?v=4?s=100" width="100px;" alt="MorielHarush"/><br /><sub><b>MorielHarush</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=MorielHarush" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://doruk.ch"><img src="https://avatars.githubusercontent.com/u/77903714?v=4?s=100" width="100px;" alt="Peak Twilight"/><br /><sub><b>Peak Twilight</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=peaktwilight" 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
+1 -1
View File
@@ -8,7 +8,7 @@ const engine = new Liquid({
// layout files for `{% layout %}`
layouts: process.cwd() + '/layouts',
// partial files for `{% include %}` and `{% render %}`
partials: process.cwd() + '/partials'
partials: [process.cwd() + '/partials', 'node_modules']
})
const ctx = {
+1 -1
View File
@@ -1,3 +1,3 @@
set -ex
set -e
npm start | grep 'LiquidJS Demo'
+1 -1
View File
@@ -1,4 +1,4 @@
set -x
set -e
LOG_FILE=$(mktemp)
npm start > $LOG_FILE 2>&1 &
+1 -1
View File
@@ -1,3 +1,3 @@
set -ex
set -e
npm start | grep 'NodeJS Demo for LiquidJS'
+1 -1
View File
@@ -1,3 +1,3 @@
set -ex
set -e
npm start | grep '\[11:8] {{ todo }}'
+1 -1
View File
@@ -1,3 +1,3 @@
set -ex
set -e
npm run build && npm start | grep 'TypeScript Demo for LiquidJS'
+1 -1
View File
@@ -1,4 +1,4 @@
set -ex
set -e
npm run build
npm start | grep 'Webpack Demo for LiquidJS'
+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
```
+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
+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/
+2
View File
@@ -24,6 +24,7 @@ Though we're trying to be compatible with the Ruby version, there are still some
* Truthy and Falsy. All values except `undefined`, `null`, `false` are truthy, whereas in Ruby Liquid all except `nil` and `false` are truthy. See [#26][#26].
* Number. In JavaScript we cannot distinguish or convert between `float` and `integer`, see [#59][#59]. And when applied `size` filter, numbers always return 0, which is 8 for integer in ruby, cause they do not have a `length` property.
* Stringify: We've aligned string coercion for primitive types. While some differences remain; for example, in Shopify/liquid, `strip` returns the "inspected" string of an input array, whereas in LiquidJS, the `strip` filter simply stringifies the input array [#852][#852].
* [.to_liquid()](https://github.com/Shopify/liquid/wiki/Introduction-to-Drops) is replaced by `.toLiquid()`
* [.to_s()](https://www.rubydoc.info/gems/liquid/Liquid/Drop) is replaced by JavaScript `.toString()`
* Iteration order for objects. The iteration order of JavaScript objects, and thus LiquidJS objects, is a combination of the insertion order for string keys, and ascending order for number-like keys, while the iteration order of Ruby Hash is simply the insertion order.
@@ -47,6 +48,7 @@ Though we're trying to be compatible with the Ruby version, there are still some
[#236]: https://github.com/harttle/liquidjs/issues/236
[#414]: https://github.com/harttle/liquidjs/discussions/414
[#485]: https://github.com/harttle/liquidjs/discussions/485
[#852]: https://github.com/harttle/liquidjs/discussions/852
[sort]: https://liquidjs.com/filters/sort.html
[stable-sort]: https://v8.dev/features/stable-sort
[plugins]: ./plugins.html#Plugin-List
+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'));
```
```
+7 -13
View File
@@ -45,26 +45,20 @@ It can be a string-typed path (see above example), or a list of root directories
```javascript
var engine = new Liquid({
root: ['views/', 'views/partials/'],
root: ['views/'],
partials: ['views/partials/'],
layouts: ['views/layouts/'],
extname: '.liquid'
});
```
{% note tip Relative Paths %}Relative paths in <code>root</code> will be resolved against <code>cwd()</code>.{% endnote %}
When `{% raw %}{% render "foo" %}{% endraw %}` is rendered or `liquid.renderFile('foo')` is called, the following files will be looked up and the first existing file will be used:
- When `parse()`, `render()` functions are called, for example `liquid.renderFile('foo')`, templates under `root` will be looked up.
- When a partial is requested, for example `{% raw %}{% render "foo" %}{% endraw %}`, templates under `partials` will be looked up.
- When a layout is requested, for example `{% raw %}{% layout "foo" %}{% endraw %}`, templates under `layouts` will be looked up.
- `cwd()`/views/foo.liquid
- `cwd()`/views/partials/foo.liquid
If none of the above files exists, an `ENOENT` error will be thrown. Here's a demo for Node.js: [demo/nodejs](https://github.com/harttle/liquidjs/tree/master/demo/nodejs).
When LiquidJS is used in browser, say current location is <https://example.com/bar/index.html>, only the first `root` will be used and the file to be fetched is:
- <https://example.com/bar/foo.liquid>
If fetch fails, a 404/500 error or network failures for example, an `ENOENT` error will be thrown.
Here's a demo for browsers: [demo/browser](https://github.com/harttle/liquidjs/tree/master/demo/browser).
When LiquidJS is used in browser, the paths will be resolved based on current location. Here's a demo for browsers: [demo/browser](https://github.com/harttle/liquidjs/tree/master/demo/browser).
## Abstract File System
@@ -24,6 +24,7 @@ LiquidJS 一直很重视兼容于 Ruby 版本的 Liquid。Liquid 模板语言最
* 真和假。在 LiquidJS 中 `undefined`, `null`, `false` 是假,之外的都是真;在 Ruby 中 `nil``false` 是假,其他都是真。见 [#26][#26]。
* 数字。JavaScript 不区分浮点数和整数,因此缺失一部分整数算术,见 [#59][#59]。此外 `size` 过滤器作用于数字时总是返回零,而不是 Ruby 中的浮点数或整数的内存大小。
* 输出字符串。基本类型的输出已经和 Shopify/liquid 对齐,但是仍然存在一些区别。比如在 Shopify/liquid 中 `strip` 会返回 inspect 字符串,但 LiquidJS `strip` 只是简单地把输入转换为字符串 [#852][#852]。
* Drop 中的 [.to_liquid()](https://github.com/Shopify/liquid/wiki/Introduction-to-Drops) 替换为 `.toLiquid()`
* 数据的 [.to_s()](https://www.rubydoc.info/gems/liquid/Liquid/Drop) 替换为 `.toString()`
* 对象的迭代顺序。JavaScript 对象的迭代顺序是插入顺序和数字键递增顺序的组合,但 Ruby Hash 中只是插入顺序(JavaScript 字面量 Object 和 Ruby 字面量 Hash 的插入顺序解释也不同)。
@@ -46,6 +47,7 @@ LiquidJS 一直很重视兼容于 Ruby 版本的 Liquid。Liquid 模板语言最
[#236]: https://github.com/harttle/liquidjs/issues/236
[#414]: https://github.com/harttle/liquidjs/discussions/414
[#485]: https://github.com/harttle/liquidjs/discussions/485
[#852]: https://github.com/harttle/liquidjs/discussions/852
[sort]: https://liquidjs.com/filters/sort.html
[stable-sort]: https://v8.dev/features/stable-sort
[plugins]: ./plugins.html#插件列表
+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>
+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
+10153 -5190
View File
File diff suppressed because it is too large Load Diff
+7 -4
View File
@@ -1,6 +1,6 @@
{
"name": "liquidjs",
"version": "10.21.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\" .",
@@ -68,7 +68,7 @@
"@semantic-release/changelog": "^6.0.2",
"@semantic-release/commit-analyzer": "^9.0.2",
"@semantic-release/git": "^10.0.1",
"@semantic-release/npm": "^9.0.2",
"@semantic-release/npm": "^13.1.5",
"@semantic-release/release-notes-generator": "^10.0.3",
"@types/benchmark": "^1.0.31",
"@types/express": "^4.17.2",
@@ -100,7 +100,7 @@
"rollup-plugin-typescript2": "^0.31.1",
"rollup-plugin-uglify": "^6.0.4",
"rollup-plugin-version-injector": "^1.3.3",
"semantic-release": "^19.0.3",
"semantic-release": "^25.0.3",
"sinon": "^15.0.2",
"supertest": "^3.4.2",
"ts-jest": "^29.0.5",
@@ -151,6 +151,9 @@
]
]
},
"publishConfig": {
"provenance": true
},
"nyc": {
"extension": [
".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
}
}
+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)
}
+12 -11
View File
@@ -43,25 +43,26 @@ export class Loader {
public * candidates (file: string, dirs: string[], currentFile?: string, enforceRoot?: boolean) {
const { fs, extname } = this.options
const isAllowed = (filepath: string) => {
if (!enforceRoot) return true
for (const dir of dirs) {
if (this.contains(dir, filepath)) return true
}
return false
}
if (this.shouldLoadRelative(file) && currentFile) {
const referenced = fs.resolve(this.dirname(currentFile), file, extname)
for (const dir of dirs) {
if (!enforceRoot || this.contains(dir, referenced)) {
// the relatively referenced file is within one of root dirs
yield referenced
break
}
}
if (isAllowed(referenced)) yield referenced
}
for (const dir of dirs) {
const referenced = fs.resolve(dir, file, extname)
if (!enforceRoot || this.contains(dir, referenced)) {
yield referenced
}
if (isAllowed(referenced)) yield referenced
}
if (fs.fallback !== undefined) {
const filepath = fs.fallback(file)
if (filepath !== undefined) yield filepath
if (filepath !== undefined && isAllowed(filepath)) yield filepath
}
}
+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'
+1 -1
View File
@@ -422,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())) {
+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
}
+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)
}
}
+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 () {
+20
View File
@@ -1,4 +1,6 @@
import { TopLevelToken, TagToken, Tokenizer, Context, Liquid, Drop, toValueSync, LiquidError, IfTag } from '../..'
import { spawnSync } from 'child_process'
import { resolve as resolvePath } from 'path'
const LiquidUMD = require('../../dist/liquid.browser.umd.js').Liquid
describe('Issues', function () {
@@ -173,6 +175,24 @@ describe('Issues', function () {
const html = await engine.render(tpl, { my_variable: 'foo' })
expect(html).toBe('CONTENT for /tmp/prefix/foo-bar/suffix')
})
it('should prevent path traversal in dynamic include with restricted root, #851', () => {
const projectRoot = resolvePath(__dirname, '../..')
const poc = `
const { Liquid } = require('./dist/liquid.node.js');
const e = new Liquid({ root: ['/tmp'], partials: ['/tmp'], dynamicPartials: true });
e.parseAndRender('{% include page %}', { page: '../../../etc/passwd' })
.then(() => { console.log('OK'); })
.catch(err => { console.error('ERR:' + err.message); process.exit(1); });
`
const result = spawnSync(
process.execPath,
['-e', poc],
{ cwd: projectRoot, encoding: 'utf8' }
)
expect(result.status).not.toBe(0)
expect(result.stderr).toContain('Failed to lookup')
})
it('Implement liquid/echo tags #428', () => {
const template = `{%- liquid
for value in array
+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)
})
})
})
+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'))
})
+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 %}'
+1 -2
View File
@@ -10,8 +10,7 @@
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"downlevelIteration": true,
"strict": true,
"suppressImplicitAnyIndexErrors": true
"strict": true
},
"all": true
}