Compare commits

...
47 Commits
Author SHA1 Message Date
semantic-release-bot 0aca2dd8c1 chore(release): 9.39.0 [skip ci]
# [9.39.0](https://github.com/harttle/liquidjs/compare/v9.38.0...v9.39.0) (2022-07-09)

### Bug Fixes

* for tag not respecting Drop#valueOf(), fixes [#515](https://github.com/harttle/liquidjs/issues/515) ([c3e51ca](https://github.com/harttle/liquidjs/commit/c3e51caa701fd4449ed5257e23569a37ef12dea2))

### Features

* iteration protocols ([a19feea](https://github.com/harttle/liquidjs/commit/a19feea7c46fc476139a150bda051f485328afe8))
2022-07-09 16:31:12 +00:00
Harttle 4289b4e804 refactor: rename toValue to toValueSync 2022-07-10 00:24:00 +08:00
Harttle c3e51caa70 fix: for tag not respecting Drop#valueOf(), fixes #515 2022-07-09 22:21:51 +08:00
James PriorandJun Yang a19feea7c4 feat: iteration protocols 2022-07-09 18:21:54 +08:00
semantic-release-bot e87f068067 chore(release): 9.38.0 [skip ci]
# [9.38.0](https://github.com/harttle/liquidjs/compare/v9.37.0...v9.38.0) (2022-07-07)

### Bug Fixes

* stack overflow on large number of templates, [#513](https://github.com/harttle/liquidjs/issues/513) ([3dc4290](https://github.com/harttle/liquidjs/commit/3dc4290b56265cfafbee8d9836e912d9b8492f90))

### Features

* inline comment tag ([#514](https://github.com/harttle/liquidjs/issues/514)) ([2f87708](https://github.com/harttle/liquidjs/commit/2f8770898963e35ac4491f6975a8abd03dc09067))
2022-07-07 19:06:27 +00:00
dependabot[bot]andJun Yang 392b859381 chore(deps): bump moment from 2.29.2 to 2.29.4 in /docs
Bumps [moment](https://github.com/moment/moment) from 2.29.2 to 2.29.4.
- [Release notes](https://github.com/moment/moment/releases)
- [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.29.2...2.29.4)

---
updated-dependencies:
- dependency-name: moment
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2022-07-08 03:00:36 +08:00
Harttle ed95d9a760 chore: update to node 16 2022-07-08 02:56:39 +08:00
Harttle 0f824355fa docs: zh-cn doc for inline comment 2022-07-08 01:59:09 +08:00
Harttle 3dc4290b56 fix: stack overflow on large number of templates, #513 2022-07-08 01:46:51 +08:00
jg-rpandGitHub 2f87708989 feat: inline comment tag (#514)
* style: remove unnecessary intermediate constant

* feat: add inline comment tag

* refactor: use readIdentifier when reading tag names

* docs: add inline comment tag
2022-07-08 01:46:15 +08:00
dependabot[bot]andJun Yang 4a0186d62a chore(deps-dev): bump jsdom from 13.2.0 to 16.5.0
Bumps [jsdom](https://github.com/jsdom/jsdom) from 13.2.0 to 16.5.0.
- [Release notes](https://github.com/jsdom/jsdom/releases)
- [Changelog](https://github.com/jsdom/jsdom/blob/master/Changelog.md)
- [Commits](https://github.com/jsdom/jsdom/compare/13.2.0...16.5.0)

---
updated-dependencies:
- dependency-name: jsdom
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <[email protected]>
2022-07-02 20:59:55 +08:00
dependabot[bot]andJun Yang c9e4220c88 chore(deps-dev): bump semantic-release from 17.2.3 to 19.0.3
Bumps [semantic-release](https://github.com/semantic-release/semantic-release) from 17.2.3 to 19.0.3.
- [Release notes](https://github.com/semantic-release/semantic-release/releases)
- [Commits](https://github.com/semantic-release/semantic-release/compare/v17.2.3...v19.0.3)

---
updated-dependencies:
- dependency-name: semantic-release
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <[email protected]>
2022-06-10 23:49:21 +08:00
Jun YangandGitHub bb8f8f569d docs: update security policy 2022-05-17 23:53:20 +08:00
HarttleandHarttle 2f6d84befc test: cases for async variables 2022-05-03 13:31:25 +08:00
semantic-release-bot 463a165e5e chore(release): 9.37.0 [skip ci]
# [9.37.0](https://github.com/harttle/liquidjs/compare/v9.36.2...v9.37.0) (2022-04-21)

### Bug Fixes

* support integer arithmetic for `divided_by`, closes [#465](https://github.com/harttle/liquidjs/issues/465) ([e69a510](https://github.com/harttle/liquidjs/commit/e69a51025efa7dec7d60d0067200a1466988ebbc))

### Features

* automatic output escaping, closes [#500](https://github.com/harttle/liquidjs/issues/500) ([f88490c](https://github.com/harttle/liquidjs/commit/f88490cd3cd0b5316c43c3ca76837544d99ce0b0))
2022-04-21 18:23:02 +00:00
Harttle f88490cd3c feat: automatic output escaping, closes #500 2022-04-22 02:19:36 +08:00
Harttle e69a51025e fix: support integer arithmetic for divided_by, closes #465 2022-04-22 02:19:36 +08:00
allcontributors[bot]andJun Yang ace5309c08 docs: update .all-contributorsrc [skip ci] 2022-04-19 23:55:10 +08:00
allcontributors[bot]andJun Yang 5a90c5bf74 docs: update README.md [skip ci] 2022-04-19 23:55:10 +08:00
semantic-release-bot f6e047867e chore(release): 9.36.2 [skip ci]
## [9.36.2](https://github.com/harttle/liquidjs/compare/v9.36.1...v9.36.2) (2022-04-19)

### Bug Fixes

* lazy createRequire to allow exceptions being catched, fixes [#497](https://github.com/harttle/liquidjs/issues/497) ([b377dad](https://github.com/harttle/liquidjs/commit/b377dad9e34c3de3e0eedc1ee6f17a0e9d919669))
2022-04-19 15:51:00 +00:00
Yahang WuandGitHub c467630e0b docs: fix a broken link for abstract file interface (#498) 2022-04-19 23:48:25 +08:00
Harttle b377dad9e3 fix: lazy createRequire to allow exceptions being catched, fixes #497 2022-04-19 23:44:14 +08:00
semantic-release-bot ecc04c99c5 chore(release): 9.36.1 [skip ci]
## [9.36.1](https://github.com/harttle/liquidjs/compare/v9.36.0...v9.36.1) (2022-04-17)

### Bug Fixes

* contains operator does not support Drop, fixes [#492](https://github.com/harttle/liquidjs/issues/492) ([9e024ff](https://github.com/harttle/liquidjs/commit/9e024ff2bcf17e7ac19c718389d4cef39b8a51f7))
* responsive header ([a56af6b](https://github.com/harttle/liquidjs/commit/a56af6bbfb5b698db55fbeb87e8e0872688fbe19))
* use `createRequire` for ESM, fixes [#334](https://github.com/harttle/liquidjs/issues/334) ([eec381e](https://github.com/harttle/liquidjs/commit/eec381ec72db3858452799b7a3264e240be3044d))
2022-04-17 16:45:54 +00:00
Harttle 9e024ff2bc fix: contains operator does not support Drop, fixes #492 2022-04-18 00:43:18 +08:00
Harttle eec381ec72 fix: use createRequire for ESM, fixes #334 2022-04-18 00:43:17 +08:00
dependabot[bot]andJun Yang c7216c6358 chore(deps): bump ansi-regex from 4.1.0 to 4.1.1 in /docs
Bumps [ansi-regex](https://github.com/chalk/ansi-regex) from 4.1.0 to 4.1.1.
- [Release notes](https://github.com/chalk/ansi-regex/releases)
- [Commits](https://github.com/chalk/ansi-regex/compare/v4.1.0...v4.1.1)

---
updated-dependencies:
- dependency-name: ansi-regex
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2022-04-18 00:43:02 +08:00
dependabot[bot]andJun Yang fe78890d35 chore(deps): bump minimist from 1.2.5 to 1.2.6
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2022-04-17 23:54:16 +08:00
dependabot[bot]andJun Yang d5771ba42c chore(deps): bump moment from 2.24.0 to 2.29.2 in /docs
Bumps [moment](https://github.com/moment/moment) from 2.24.0 to 2.29.2.
- [Release notes](https://github.com/moment/moment/releases)
- [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.24.0...2.29.2)

---
updated-dependencies:
- dependency-name: moment
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2022-04-17 22:02:49 +08:00
dependabot[bot]andJun Yang 776d235fca chore(deps): bump async from 3.1.0 to 3.2.3
Bumps [async](https://github.com/caolan/async) from 3.1.0 to 3.2.3.
- [Release notes](https://github.com/caolan/async/releases)
- [Changelog](https://github.com/caolan/async/blob/master/CHANGELOG.md)
- [Commits](https://github.com/caolan/async/compare/v3.1.0...v3.2.3)

---
updated-dependencies:
- dependency-name: async
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2022-04-17 21:55:18 +08:00
Harttle a56af6bbfb fix: responsive header 2022-03-17 16:19:57 +08:00
semantic-release-bot 1af8553651 chore(release): 9.36.0 [skip ci]
# [9.36.0](https://github.com/harttle/liquidjs/compare/v9.35.2...v9.36.0) (2022-03-05)

### Features

* Access array item by negative index, closes [#486](https://github.com/harttle/liquidjs/issues/486) ([049685b](https://github.com/harttle/liquidjs/commit/049685b9a0271ba03875e24ff2f6c7870cae62a7))
* allow strip filter with specified char, closes [#390](https://github.com/harttle/liquidjs/issues/390) ([c503cb2](https://github.com/harttle/liquidjs/commit/c503cb23dfbdd6f146d6dea16a84eab3df1f7aa9))
* appropriate error for malformed filters, fixes [#271](https://github.com/harttle/liquidjs/issues/271) ([01014ed](https://github.com/harttle/liquidjs/commit/01014edc491e12d38981045442da9faee598cdf7))
2022-03-05 16:56:21 +00:00
Harttle 049685b9a0 feat: Access array item by negative index, closes #486 2022-03-06 00:52:22 +08:00
Harttle c503cb23df feat: allow strip filter with specified char, closes #390 2022-03-06 00:20:51 +08:00
Harttle 01014edc49 feat: appropriate error for malformed filters, fixes #271 2022-03-05 23:24:16 +08:00
Harttle 27e579e0e2 test: case for {% assign %} invalid value, closes #446 2022-03-05 22:09:36 +08:00
semantic-release-bot d3e3fecf53 chore(release): 9.35.2 [skip ci]
## [9.35.2](https://github.com/harttle/liquidjs/compare/v9.35.1...v9.35.2) (2022-03-02)

### Bug Fixes

* corner case for concat filter without argument, [#481](https://github.com/harttle/liquidjs/issues/481) ([aa95517](https://github.com/harttle/liquidjs/commit/aa955173d4c7adc585e862934429f1f4c5f64969))
* export all builtin tags from LiquidJS, [#464](https://github.com/harttle/liquidjs/issues/464) ([33009bb](https://github.com/harttle/liquidjs/commit/33009bb988eb74c58f390992750d91b967cb3428))
2022-03-02 13:26:07 +00:00
Harttle 33009bb988 fix: export all builtin tags from LiquidJS, #464 2022-03-02 21:21:45 +08:00
Harttle aa955173d4 fix: corner case for concat filter without argument, #481 2022-03-02 21:21:25 +08:00
semantic-release-bot 15b78ebd9d chore(release): 9.35.1 [skip ci]
## [9.35.1](https://github.com/harttle/liquidjs/compare/v9.35.0...v9.35.1) (2022-02-26)

### Bug Fixes

* some filters throw on nil input, see [#481](https://github.com/harttle/liquidjs/issues/481) ([7dfb620](https://github.com/harttle/liquidjs/commit/7dfb620d30f8818685e1cfb8e7492313a0d036ab))
2022-02-26 18:12:38 +00:00
Harttle 575f3e2338 docs: add missing tag/filter links on sidebar/table, #482 2022-02-27 02:07:59 +08:00
Harttle 7dfb620d30 fix: some filters throw on nil input, see #481 2022-02-27 02:07:59 +08:00
dependabot[bot]andJun Yang 21b78d9ba0 chore(deps): bump prismjs from 1.25.0 to 1.27.0 in /docs
Bumps [prismjs](https://github.com/PrismJS/prism) from 1.25.0 to 1.27.0.
- [Release notes](https://github.com/PrismJS/prism/releases)
- [Changelog](https://github.com/PrismJS/prism/blob/master/CHANGELOG.md)
- [Commits](https://github.com/PrismJS/prism/compare/v1.25.0...v1.27.0)

---
updated-dependencies:
- dependency-name: prismjs
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2022-02-27 02:06:04 +08:00
Harttle 5d077f461a chore: add liquid.min.js into Github release 2022-02-24 01:39:27 +08:00
semantic-release-bot 13343040c6 chore(release): 9.35.0 [skip ci]
# [9.35.0](https://github.com/harttle/liquidjs/compare/v9.34.1...v9.35.0) (2022-02-23)

### Bug Fixes

* `url_encode` throws on undefined value, fixes [#479](https://github.com/harttle/liquidjs/issues/479) ([ca3240c](https://github.com/harttle/liquidjs/commit/ca3240c2c4d157095d2ebe0024d0c71bc5e435f8))

### Features

* expose all tags/filters and TimezoneDate, closes [#464](https://github.com/harttle/liquidjs/issues/464) ([dab8a29](https://github.com/harttle/liquidjs/commit/dab8a29070b2508f2e6532717b7663966f610bec))
2022-02-23 17:20:40 +00:00
Harttle ca3240c2c4 fix: url_encode throws on undefined value, fixes #479 2022-02-24 01:14:46 +08:00
Harttle 2b0c5696bc docs: introduce JS script to escape for Hexo, fixes #480 2022-02-24 01:04:50 +08:00
Harttle dab8a29070 feat: expose all tags/filters and TimezoneDate, closes #464 2022-02-23 23:38:21 +08:00
98 changed files with 24177 additions and 3909 deletions
+9
View File
@@ -486,6 +486,15 @@
"contributions": [
"doc"
]
},
{
"login": "YahangWu",
"name": "Yahang Wu",
"avatar_url": "https://avatars.githubusercontent.com/u/12295975?v=4",
"profile": "https://github.com/YahangWu",
"contributions": [
"doc"
]
}
],
"contributorsPerLine": 7,
+88
View File
@@ -1,3 +1,91 @@
# [9.39.0](https://github.com/harttle/liquidjs/compare/v9.38.0...v9.39.0) (2022-07-09)
### Bug Fixes
* for tag not respecting Drop#valueOf(), fixes [#515](https://github.com/harttle/liquidjs/issues/515) ([c3e51ca](https://github.com/harttle/liquidjs/commit/c3e51caa701fd4449ed5257e23569a37ef12dea2))
### Features
* iteration protocols ([a19feea](https://github.com/harttle/liquidjs/commit/a19feea7c46fc476139a150bda051f485328afe8))
# [9.38.0](https://github.com/harttle/liquidjs/compare/v9.37.0...v9.38.0) (2022-07-07)
### Bug Fixes
* stack overflow on large number of templates, [#513](https://github.com/harttle/liquidjs/issues/513) ([3dc4290](https://github.com/harttle/liquidjs/commit/3dc4290b56265cfafbee8d9836e912d9b8492f90))
### Features
* inline comment tag ([#514](https://github.com/harttle/liquidjs/issues/514)) ([2f87708](https://github.com/harttle/liquidjs/commit/2f8770898963e35ac4491f6975a8abd03dc09067))
# [9.37.0](https://github.com/harttle/liquidjs/compare/v9.36.2...v9.37.0) (2022-04-21)
### Bug Fixes
* support integer arithmetic for `divided_by`, closes [#465](https://github.com/harttle/liquidjs/issues/465) ([e69a510](https://github.com/harttle/liquidjs/commit/e69a51025efa7dec7d60d0067200a1466988ebbc))
### Features
* automatic output escaping, closes [#500](https://github.com/harttle/liquidjs/issues/500) ([f88490c](https://github.com/harttle/liquidjs/commit/f88490cd3cd0b5316c43c3ca76837544d99ce0b0))
## [9.36.2](https://github.com/harttle/liquidjs/compare/v9.36.1...v9.36.2) (2022-04-19)
### Bug Fixes
* lazy createRequire to allow exceptions being catched, fixes [#497](https://github.com/harttle/liquidjs/issues/497) ([b377dad](https://github.com/harttle/liquidjs/commit/b377dad9e34c3de3e0eedc1ee6f17a0e9d919669))
## [9.36.1](https://github.com/harttle/liquidjs/compare/v9.36.0...v9.36.1) (2022-04-17)
### Bug Fixes
* contains operator does not support Drop, fixes [#492](https://github.com/harttle/liquidjs/issues/492) ([9e024ff](https://github.com/harttle/liquidjs/commit/9e024ff2bcf17e7ac19c718389d4cef39b8a51f7))
* responsive header ([a56af6b](https://github.com/harttle/liquidjs/commit/a56af6bbfb5b698db55fbeb87e8e0872688fbe19))
* use `createRequire` for ESM, fixes [#334](https://github.com/harttle/liquidjs/issues/334) ([eec381e](https://github.com/harttle/liquidjs/commit/eec381ec72db3858452799b7a3264e240be3044d))
# [9.36.0](https://github.com/harttle/liquidjs/compare/v9.35.2...v9.36.0) (2022-03-05)
### Features
* Access array item by negative index, closes [#486](https://github.com/harttle/liquidjs/issues/486) ([049685b](https://github.com/harttle/liquidjs/commit/049685b9a0271ba03875e24ff2f6c7870cae62a7))
* allow strip filter with specified char, closes [#390](https://github.com/harttle/liquidjs/issues/390) ([c503cb2](https://github.com/harttle/liquidjs/commit/c503cb23dfbdd6f146d6dea16a84eab3df1f7aa9))
* appropriate error for malformed filters, fixes [#271](https://github.com/harttle/liquidjs/issues/271) ([01014ed](https://github.com/harttle/liquidjs/commit/01014edc491e12d38981045442da9faee598cdf7))
## [9.35.2](https://github.com/harttle/liquidjs/compare/v9.35.1...v9.35.2) (2022-03-02)
### Bug Fixes
* corner case for concat filter without argument, [#481](https://github.com/harttle/liquidjs/issues/481) ([aa95517](https://github.com/harttle/liquidjs/commit/aa955173d4c7adc585e862934429f1f4c5f64969))
* export all builtin tags from LiquidJS, [#464](https://github.com/harttle/liquidjs/issues/464) ([33009bb](https://github.com/harttle/liquidjs/commit/33009bb988eb74c58f390992750d91b967cb3428))
## [9.35.1](https://github.com/harttle/liquidjs/compare/v9.35.0...v9.35.1) (2022-02-26)
### Bug Fixes
* some filters throw on nil input, see [#481](https://github.com/harttle/liquidjs/issues/481) ([7dfb620](https://github.com/harttle/liquidjs/commit/7dfb620d30f8818685e1cfb8e7492313a0d036ab))
# [9.35.0](https://github.com/harttle/liquidjs/compare/v9.34.1...v9.35.0) (2022-02-23)
### Bug Fixes
* `url_encode` throws on undefined value, fixes [#479](https://github.com/harttle/liquidjs/issues/479) ([ca3240c](https://github.com/harttle/liquidjs/commit/ca3240c2c4d157095d2ebe0024d0c71bc5e435f8))
### Features
* expose all tags/filters and TimezoneDate, closes [#464](https://github.com/harttle/liquidjs/issues/464) ([dab8a29](https://github.com/harttle/liquidjs/commit/dab8a29070b2508f2e6532717b7663966f610bec))
## [9.34.1](https://github.com/harttle/liquidjs/compare/v9.34.0...v9.34.1) (2022-02-20)
+1
View File
@@ -139,6 +139,7 @@ Want to contribute? see [Contribution Guidelines][contribution]. Thanks goes to
<td align="center"><a href="https://github.com/ameyaapte1"><img src="https://avatars.githubusercontent.com/u/16054747?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ameya Apte</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=ameyaapte1" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/tbdrz"><img src="https://avatars.githubusercontent.com/u/50599116?v=4?s=100" width="100px;" alt=""/><br /><sub><b>tbdrz</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=tbdrz" title="Documentation">📖</a></td>
<td align="center"><a href="http://santialbo.com"><img src="https://avatars.githubusercontent.com/u/1557563?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Santi Albo</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=santialbo" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/YahangWu"><img src="https://avatars.githubusercontent.com/u/12295975?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Yahang Wu</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=YahangWu" title="Documentation">📖</a></td>
</tr>
</table>
+4 -4
View File
@@ -6,8 +6,8 @@ Only the latest major version is supported with security updates. It can be chan
## Reporting a Vulnerability
Please contact yangjvn@126.com to report a vulnerability.
Please contact yangjvn@126.com to report a vulnerability or change request.
- It will be fixed very soon (maybe within 1 week or couple of days) if that vulnerability obviously affects common use cases.
- Otherwise, it'll be scheduled with a priority just like requested features (which is lower than bugs).
- If it's declined, you'll receive an email (more oftenly there will be a discuss).
- If the vulnerability in question affects common use cases, it will be treated as a bug and fixed very soon (typically within 1 week).
- Otherwise, it'll be scheduled in the same priority of feature request (which is lower than bugs).
- If the request is declined, you'll receive a reply email anyway (most likely there will be a discussion).
+1 -5
View File
@@ -11,8 +11,6 @@ for file in $(find docs/source/api -name "*.md"); do
-e 's/\(\]([^_)]*\)\/_/\1\//g' \
-e 's/\(\]([^)]*\.\)md/\1html/g' \
-e 's/\](_/\](/g' \
-e 's/{%/{% raw %}{%{% endraw %}/g' \
-e 's/{{/{% raw %}{{{% endraw %}/g' \
-e '1 s/"/\&quot;/g' \
-e '1 s/</\&lt;/g' \
-e '1 s/>/\&gt;/g' \
@@ -20,9 +18,7 @@ for file in $(find docs/source/api -name "*.md"); do
-e '1 s/^# \(.*\)/---\ntitle: "\1"\nauto: true\n---/' \
$file
target=${file/\/_/\/}
if [ "$file" != "$target" ]; then
mv $file $target
fi
cat $file | ./bin/escape-nunjucks.js > $target
done
cp -r docs/source/api docs/source/zh-cn/api
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env node
// In Hexo, content inside markdown code/code fences,
// will not go through nunjucks.
// So we're escaping special chars outside code only.
let backTick = 0
process.stdin.on('data', (buffer) => {
for (const c of buffer.toString()) {
if (c === '`') {
backTick++
}
const shouldEscape = '{%%}'.includes(c) && backTick % 2 === 0
if (shouldEscape) {
const code = c.charCodeAt(0).toString(16)
process.stdout.write(`&#x${code};`)
} else process.stdout.write(c)
}
})
+9
View File
@@ -0,0 +1,9 @@
# LiquidJS for Node.js
## Get Started
```bash
cd demo/nodejs
npm install
npm start
```
+30
View File
@@ -0,0 +1,30 @@
import { Liquid } from 'liquidjs'
const engine = new Liquid({
extname: '.liquid',
globals: { title: 'LiquidJS Demo' },
// root files for `.render()` and `.parse()`
root: process.cwd(),
// layout files for `{% layout %}`
layouts: process.cwd() + '/layouts',
// partial files for `{% include %}` and `{% render %}`
partials: process.cwd() + '/partials'
})
const ctx = {
todos: ['fork and clone', 'make it better', 'make a pull request']
}
async function main () {
console.log('==========renderFile===========')
const html = await engine.renderFile('todolist', ctx)
console.log(html)
console.log('===========Streamed===========')
const tpls = await engine.parseFile('todolist')
engine.renderToNodeStream(tpls, ctx)
.on('data', data => process.stdout.write(data))
.on('end', () => console.log(''))
}
main()
+7
View File
@@ -0,0 +1,7 @@
<html>
<head><meta name="title" content="{{title}}"></head>
<body>
{% block %}{% endblock %}
{% render "footer.liquid" %}
</body>
</html>
+13
View File
@@ -0,0 +1,13 @@
{
"name": "liquidjs-demo-nodejs",
"description": "Node.js demo for liquidjs",
"private": true,
"main": "index.mjs",
"scripts": {
"start": "node index.mjs"
},
"dependencies": {
"hello-liquid": "^1.0.0",
"liquidjs": "^9.28.3"
}
}
+1
View File
@@ -0,0 +1 @@
<small>{{title}}</small>
+1
View File
@@ -0,0 +1 @@
<li>{{index}} - {{todo}}</li>
+7
View File
@@ -0,0 +1,7 @@
{% layout 'html.liquid' %}
<h1>{% include "hello-liquid" %}</h1>
<ul>
{% for todo in todos %}
{% render 'todo.liquid' with todo, index: forloop.index%}
{% endfor %}
</ul>
+33 -30
View File
@@ -350,9 +350,9 @@
}
},
"node_modules/ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"engines": {
"node": ">=8"
}
@@ -1256,9 +1256,9 @@
}
},
"node_modules/eslint/node_modules/ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz",
"integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==",
"dev": true,
"engines": {
"node": ">=6"
@@ -3428,9 +3428,9 @@
}
},
"node_modules/moment": {
"version": "2.24.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
"integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==",
"version": "2.29.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
"engines": {
"node": "*"
}
@@ -3893,9 +3893,12 @@
}
},
"node_modules/prismjs": {
"version": "1.25.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.25.0.tgz",
"integrity": "sha512-WCjJHl1KEWbnkQom1+SzftbtXMKQoezOCYs5rECqMN+jP+apI7ftoflyqigqzopSO3hMhTEb0mFClA8lkolgEg=="
"version": "1.27.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz",
"integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==",
"engines": {
"node": ">=6"
}
},
"node_modules/process": {
"version": "0.11.10",
@@ -4682,9 +4685,9 @@
}
},
"node_modules/table/node_modules/ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz",
"integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==",
"dev": true,
"engines": {
"node": ">=6"
@@ -5416,9 +5419,9 @@
}
},
"ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg=="
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
},
"ansi-styles": {
"version": "4.2.1",
@@ -6101,9 +6104,9 @@
},
"dependencies": {
"ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz",
"integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==",
"dev": true
},
"ansi-styles": {
@@ -7866,9 +7869,9 @@
}
},
"moment": {
"version": "2.24.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
"integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
"version": "2.29.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
},
"moment-timezone": {
"version": "0.5.28",
@@ -8223,9 +8226,9 @@
"integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE="
},
"prismjs": {
"version": "1.25.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.25.0.tgz",
"integrity": "sha512-WCjJHl1KEWbnkQom1+SzftbtXMKQoezOCYs5rECqMN+jP+apI7ftoflyqigqzopSO3hMhTEb0mFClA8lkolgEg=="
"version": "1.27.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz",
"integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA=="
},
"process": {
"version": "0.11.10",
@@ -8889,9 +8892,9 @@
},
"dependencies": {
"ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz",
"integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==",
"dev": true
},
"emoji-regex": {
+5
View File
@@ -51,6 +51,7 @@ filters:
newline_to_br: newline_to_br.html
plus: plus.html
prepend: prepend.html
raw: raw.html
remove: remove.html
remove_first: remove_first.html
replace: replace.html
@@ -77,6 +78,7 @@ filters:
tags:
overview: overview.html
"# (inline comment)": inline_comment.html
assign: assign.html
capture: capture.html
case: case.html
@@ -84,6 +86,8 @@ tags:
cycle: cycle.html
decrement: decrement.html
echo: echo.html
else: if.html
elsif: if.html
for: for.html
if: if.html
include: include.html
@@ -94,6 +98,7 @@ tags:
render: render.html
tablerow: tablerow.html
unless: unless.html
when: case.html
api:
quick_entry:
+18 -1
View File
@@ -26,6 +26,23 @@ Output
1.6666666666666667
```
{% note info Integer Arithmetic %}Since JavaScript doesn't differentiate integers and floats, LiquidJS is not capable of integer arithmetic and the return type is always `number`, the string representation of which depends on its value.{% endnote %}
In JavaScript, float and integer shares the same type `number` and we cannot tell the difference. For example:
```javascript
// always true
5.0 === 5
```
You'll need to pass another `integerArithmetic` argument to enforce integer divide:
Input
```liquid
{{ 5 | divided_by: 3, true }}
```
Output
```text
1
```
[floor]: ./floor.html
+3 -2
View File
@@ -9,10 +9,11 @@ There's 40+ filters supported by LiquidJS. These filters can be categorized into
Categories | Filters
--- | ---
Math | plus, minus, modulo, times, floor, ceil, round, divided_by, abs
Math | plus, minus, modulo, times, floor, ceil, round, divided_by, abs, at_least, at_most
String | append, prepend, capitalize, upcase, downcase, strip, lstrip, rstrip, strip_newlines, split, replace, replace_first, remove, remove_first, truncate, truncatewords
HTML/URI | escape, escape_once, url_encode, url_decode, strip_html, newline_to_br
Array | slice, map, sort, sort_natural, uniq, where, first, last, join, reverse, concat
Array | slice, map, sort, sort_natural, uniq, where, first, last, join, reverse, concat, compact, size
Date | date
Misc | default, json, raw
[shopify/liquid]: https://github.com/Shopify/liquid
+51
View File
@@ -0,0 +1,51 @@
---
title: raw
---
{% since %}v9.37.0{% endsince %}
Liquid filter that directly returns the value of the variable. Useful when [outputEscape](/api/interfaces/liquid_options_.liquidoptions.html#Optional-outputEscape) is set.
{% note info Auto escape %}
By default `outputEscape` is not set. That means LiquidJS output is not escaped by default, thus `raw` filter is not useful until `outputEscape` is set.
{% endnote %}
Input (`outputEscape` not set)
```liquid
{{ "<" }}
```
Output
```text
<
```
Input (`outputEscape="escape"`)
```liquid
{{ "<" }}
```
Output
```text
&lt;
```
Input (`outputEscape="json"`)
```liquid
{{ "<" }}
```
Output
```text
"<"
```
Input (`outputEscape="escape"`)
```liquid
{{ "<" | raw }}
```
Output
```text
<
```
+50
View File
@@ -0,0 +1,50 @@
---
title: "# (inline comment)"
---
{% since %}v9.38.0{% endsince %}
Add comments to a Liquid template using an inline tag. Text enclosed in an inline comment tag will not be printed.
Input
```liquid
Anything inside an inline comment tag will not be printed.
{% # this is an inline comment %}
But every line must start with a '#'.
{%
# this is a comment
# that spans multiple lines
%}
```
Output
```text
Anything inside an inline comment tag will not be printed.
But every line must start with a '#'.
```
Inline comments are useful inside <a href="./liquid.html">`liquid`</a> tags too.
```liquid
{% liquid
# required args
assign product = collection.products.first
# optional args
assign should_show_border = should_show_border | default: true
assign should_highlight = should_highlight | default: false
%}
```
But they don't work well for commenting out blocks of Liquid code. The <a href="./comment.html">`comment`</a> block tag is the better option when you need to temporarily stop other tags from being executed.
Input
```liquid
{%- # {% echo 'Welcome to LiquidJS!' %} -%}
{% comment %}{% echo 'Welcome to LiquidJS!' %}{% endcomment %}
```
Output
```text
-%}
```
+2 -2
View File
@@ -11,8 +11,8 @@ Category | Purpose | Tags
--- | --- | ---
Iteration | iterate over a collection | for, cycle, tablerow
Control Flow | control the execution branch of template rendering | if, unless, elsif, else, case, when
Variable | define and alter variables | assign, increment, decrement
Variable | define and alter variables | assign, increment, decrement, capture, echo
File | include another template or extend a layout template | render, include, layout
Language | temporarily disable LiquidJS syntax | raw, comment
Language | temporarily disable LiquidJS syntax | # (inline comment), raw, comment, liquid
[shopify/liquid]: https://github.com/Shopify/liquid
+24 -1
View File
@@ -2,7 +2,25 @@
title: Differences with Shopify/liquid
---
All filters and tags in [shopify/liquid](https://github.com/Shopify/liquid) are supposed to be built in LiquidJS, but not those business-logic specific tags/filters which are typically from Shopify themes (see [Plugins List][plugins] in case you're looking for them and feel free to add yours to the list). Though being compatible the Ruby version is one of our priorities, there are still some differences:
## Compatibility
Being compatible the Ruby version is one of our priorities. Liquid language is originally [implemented in Ruby][ruby-liquid] and used by Shopify, Jekyll and thus Github Pages, as you can see it's one of the most popular template engines in Ruby. There're lots of people using LiquidJS to serve their templates originally written for Shopify themes and Jekyll sites.
So "being compatible" means serve developers from Shopify and Jekyll well:
- **Well-formed Liquid template should work just fine in LiquidJS**. For example, `forloop.index` should be 1-indexed, `nil` should be rendered as empty string rather than `undefined`, etc. Although some features (e.g. [#236][#236]) are not feasible in JavaScript, at least we're trying to implement all the semantics of Liquid language.
- **All filters and tags in [shopify/liquid][ruby-liquid] are supposed to be built in LiquidJS**. But not those business-logic specific tags/filters typically defined by Shopify platform. Those features should be maintained as [plugins][plugins]. For filters/tags that are not business-logic specific, like `{% layout %}`, and extremely useful, feel free to file an issue.
In the meantime, it's now implemented in JavaScript, that means it has to be more powerful:
* **Async as first-class citizen**. Filters and tags can be implemented asynchronously by return a `Promise`.
* **Also can be sync**. For scenarios that are not I/O intensive, render synchronously can be much faster. You can call synchronous APIs like `.renderSync()` as long as all the filters and tags in template support to be rendered synchronously. All builtin filters/tags support both sync and async render.
* **[Abstract file system][afs]**. Along with async feature, LiquidJS can be used to serve templates stored in Databases [#414][#414], on remote HTTP server [#485][#485], and so on.
* **Additional tags and filters** like `layout` and `json`.
## Differences
Though we're trying to be compatible with the Ruby version, there are still some differences:
* 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.
@@ -20,6 +38,11 @@ All filters and tags in [shopify/liquid](https://github.com/Shopify/liquid) are
[#59]: https://github.com/harttle/liquidjs/issues/59
[#208]: https://github.com/harttle/liquidjs/issues/208
[#212]: https://github.com/harttle/liquidjs/issues/212
[#236]: https://github.com/harttle/liquidjs/issues/236
[#414]: https://github.com/harttle/liquidjs/discussions/414
[#485]: https://github.com/harttle/liquidjs/discussions/485
[sort]: https://liquidjs.com/filters/sort.html
[stable-sort]: https://v8.dev/features/stable-sort
[plugins]: ./plugins.html#Plugin-List
[ruby-liquid]: https://github.com/Shopify/liquid
[afs]: https://liquidjs.com/tutorials/render-file.html#Abstract-File-System
+2 -2
View File
@@ -68,7 +68,7 @@ Here's a demo for browsers: [demo/browser](https://github.com/harttle/liquidjs/t
## Abstract File System
LiquidJS defines an abstract file system interface in [src/fs/ifs.ts][ifs] and the default implementation is [src/fs/node.ts][fs-node] for Node.js and [src/fs/browser.ts][fs-browser] for the browser bundle.
LiquidJS defines an abstract file system interface in [src/fs/fs.ts][ifs] and the default implementation is [src/fs/node.ts][fs-node] for Node.js and [src/fs/browser.ts][fs-browser] for the browser bundle.
The `Liquid` constructor provides a [fs][fs] option to specify the file system implementation. It's supposed to be used to define customized template fetching logic, i.e. fetch template from a database table, like:
@@ -101,7 +101,7 @@ var engine = new Liquid({
{% note warn Path Traversal Vulnerability %}The default value of <code>contains()</code> always returns true. That means when specifying an abstract file system, you'll need to provide a proper <code>contains()</code> to avoid expose such vulnerabilities.{% endnote %}
[fs]: ../api/interfaces/liquid_options_.liquidoptions.html#Optional-fs
[ifs]: https://github.com/harttle/liquidjs/blob/master/src/fs/ifs.ts
[ifs]: https://github.com/harttle/liquidjs/blob/master/src/fs/fs.ts
[fs-node]: https://github.com/harttle/liquidjs/blob/master/src/fs/node.ts
[fs-browser]: https://github.com/harttle/liquidjs/blob/master/src/fs/browser.ts
[layout]: https://help.shopify.com/en/themes/liquid/tags/theme-tags#layout
+18 -1
View File
@@ -26,6 +26,23 @@ title: divided_by
1.6666666666666667
```
{% note info Integer Arithmetic %}Since JavaScript doesn't differentiate integers and floats, LiquidJS is not capable of integer arithmetic and the return type is always `number`, the string representation of which depends on its value.{% endnote %}
在 JavaScript 里数字没有浮点和整数的区分,它们的类型都是 `number`
```javascript
// always true
5.0 === 5
```
因此如果需要做整数运算,需要传入额外的 `integerArithmetic` 参数:
Input
```liquid
{{ 5 | divided_by: 3, true }}
```
Output
```text
1
```
[floor]: ./floor.html
+3 -2
View File
@@ -9,10 +9,11 @@ LiquidJS 共支持 40+ 个过滤器,可以分为如下几类:
类别 | 过滤器
--- | ---
数学 | plus, minus, modulo, times, floor, ceil, round, divided_by, abs
数学 | plus, minus, modulo, times, floor, ceil, round, divided_by, abs, at_least, at_most
字符串 | append, prepend, capitalize, upcase, downcase, strip, lstrip, rstrip, strip_newlines, split, replace, replace_first, remove, remove_first, truncate, truncatewords
HTML/URI | escape, escape_once, url_encode, url_decode, strip_html, newline_to_br
数组 | slice, map, sort, sort_natural, uniq, wheres, first, last, join, reverse, concat
数组 | slice, map, sort, sort_natural, uniq, wheres, first, last, join, reverse, concat, compact, size
日期 | date
其他 | default, json
[shopify/liquid]: https://github.com/Shopify/liquid
+52
View File
@@ -0,0 +1,52 @@
---
title: raw
---
{% since %}v9.37.0{% endsince %}
直接返回变量的值。配合 [outputEscape](/api/interfaces/liquid_options_.liquidoptions.html#Optional-outputEscape) 参数使用。
{% note info 自动转义 %}
默认情况下 `outputEscape``undefined`,这意味着 LiquidJS 输出不会默认转义,因此这时使用 `raw` 没有意义。
{% endnote %}
输入(未设置 `outputEscape`
```liquid
{{ "<" }}
```
输出
```text
<
```
输入(`outputEscape="escape"`
```liquid
{{ "<" }}
```
输出
```text
&lt;
```
输入(`outputEscape="json"`
```liquid
{{ "<" }}
```
输出
```text
"<"
```
输入(`outputEscape="escape"`
```liquid
{{ "<" | raw }}
```
输出
```text
<
```
+50
View File
@@ -0,0 +1,50 @@
---
title: "#(单行注释)"
---
{% since %}v9.38.0{% endsince %}
在 Liquid 模板中添加注释,注释标签内的文字不会被输出。
输入
```liquid
注释标签内的东西都不会输出。
{% # this is an inline comment %}
但每行都必须以 '#' 开头。
{%
# this is a comment
# that spans multiple lines
%}
```
输出
```text
注释标签内的东西都不会输出。
但每行都必须以 '#' 开头。
```
在 <a href="./liquid.html">`liquid`</a> 标签里也可以使用注释标签。
```liquid
{% liquid
# required args
assign product = collection.products.first
# optional args
assign should_show_border = should_show_border | default: true
assign should_highlight = should_highlight | default: false
%}
```
但注释标签不能用于把其他标签注释掉。这时应该使用 <a href="./comment.html">`comment`</a> 标签来临时禁用其他标签。
输入
```liquid
{%- # {% echo 'Welcome to LiquidJS!' %} -%}
{% comment %}{% echo 'Welcome to LiquidJS!' %}{% endcomment %}
```
输出
```text
-%}
```
+2 -2
View File
@@ -11,8 +11,8 @@ LiquidJS 支持十几个过滤器,可以分为如下几类:
--- | --- | ---
迭代 | 遍历一个集合 | for, cycle, tablerow
控制流 | 控制模板渲染的执行分支 | if, unless, elif, else, case, when
变量 | 定义和修改变量 | assign, increment, decrement
变量 | 定义和修改变量 | assign, increment, decrement, capture, echo
文件 | 引入或继承其他模板 | render, include, layout
语言 | 暂时禁用 Liquid 语法 | raw, comment
语言 | 暂时禁用 Liquid 语法 | # (单行注释), raw, comment, liquid
[shopify/liquid]: https://github.com/Shopify/liquid
+24 -1
View File
@@ -2,7 +2,25 @@
title: 和 Shopify/liquid 的区别
---
[Shopify/liquid](https://github.com/Shopify/liquid) 中的所有标签和过滤器 LiquidJS 都支持,但不包括 Shopify 主题中业务逻辑相关的标签和过滤器(如果你在找这些标签可以参考 [插件列表][plugins],也欢迎把你的插件添加到列表中)。尽管原则上我们尽力兼容于 Shopify/liquid,但仍然存在一些区别:
## 兼容性
LiquidJS 一直很重视兼容于 Ruby 版本的 Liquid。Liquid 模板语言最初由 [Ruby 实现][ruby-liquid],用于 ShopifyJekyll 以及 Github Pages,它是 Ruby 里最流行的模板引擎之一。因此由很多人用 LiquidJS 来渲染他们的 Shopify 主题和 Jekyll 站点。
所以“兼容”意味着让这些开发者有很好的使用体验:
- **LiquidJS 应当能够渲染语法正确的 Liquid 模板**。例如 `forloop.index` 应该是 1 开始的下表,`nil` 应该渲染成空字符串而不是 `undefined` 等。即使有些功能(例如 [#236][#236])用 JavaScript 很难实现,但至少 LiquidJS 会尝试实现所有的 Liquid 语义。
- **所有 [shopify/liquid][ruby-liquid] 里的标签和过滤器 LiquidJS 都要实现**。这之外有些业务逻辑相关的标签/过滤器尤其是 Shopify 平台上的那些,应该维护在 [插件][plugins] 里。但是这其中有一些很有用的标签(比如 `{% layout %}`)LiquidJS 也会考虑实现,可以去开个 Issue 讨论一下。
同时,既然现在用 JavaScript 实现了,那 Liquid 应该有更强的功能:
* **完全支持异步**。所有过滤器和标签都可以实现为异步,只需要返回 `Promise` 即可。
* **同时支持同步**。对一些非 I/O 密集的场景,同步渲染会更快。只要模板包含的标签和过滤器都支持同步,你就可以调用类似 `.renderSync()` 这样的 API。所有内置标签和过滤器都同时支持同步和异步。
* **[抽象文件系统][afs]**。和异步功能一起使用,LiquidJS 可以实现渲染数据库里的模板 [#414][#414],远程 HTTP 服务器上的模板 [#485][#485],等等。
* **额外的标签和过滤器**。比如 `layout``json`
## 区别
[Shopify/liquid][ruby-liquid] 中的所有标签和过滤器 LiquidJS 都支持,但不包括 Shopify 主题中业务逻辑相关的标签和过滤器(如果你在找这些标签可以参考 [插件列表][plugins],也欢迎把你的插件添加到列表中)。尽管原则上我们尽力兼容于 Shopify/liquid,但仍然存在一些区别:
* 真和假。在 LiquidJS 中 `undefined`, `null`, `false` 是假,之外的都是真;在 Ruby 中 `nil``false` 是假,其他都是真。见 [#26][#26]。
* 数字。JavaScript 不区分浮点数和整数,因此缺失一部分整数算术,见 [#59][#59]。此外 `size` 过滤器作用于数字时总是返回零,而不是 Ruby 中的浮点数或整数的内存大小。
@@ -20,6 +38,11 @@ title: 和 Shopify/liquid 的区别
[#59]: https://github.com/harttle/liquidjs/issues/59
[#208]: https://github.com/harttle/liquidjs/issues/208
[#212]: https://github.com/harttle/liquidjs/issues/212
[#236]: https://github.com/harttle/liquidjs/issues/236
[#414]: https://github.com/harttle/liquidjs/discussions/414
[#485]: https://github.com/harttle/liquidjs/discussions/485
[sort]: https://liquidjs.com/filters/sort.html
[stable-sort]: https://v8.dev/features/stable-sort
[plugins]: ./plugins.html#插件列表
[ruby-liquid]: https://github.com/Shopify/liquid
[afs]: https://liquidjs.com/tutorials/render-file.html#Abstract-File-System
+1
View File
@@ -59,6 +59,7 @@ sidebar:
overview: 概述
tags:
overview: 概述
'# (inline comment)': '#(单行注释)'
api:
quick_entry: 快速入口
classes:
+1
View File
@@ -55,6 +55,7 @@
<td align="center"><a href="https://github.com/ameyaapte1"><img src="https://avatars.githubusercontent.com/u/16054747?v=4?s=100" width="100px;" alt=""/></a></td>
<td align="center"><a href="https://github.com/tbdrz"><img src="https://avatars.githubusercontent.com/u/50599116?v=4?s=100" width="100px;" alt=""/></a></td>
<td align="center"><a href="http://santialbo.com"><img src="https://avatars.githubusercontent.com/u/1557563?v=4?s=100" width="100px;" alt=""/></a></td>
<td align="center"><a href="https://github.com/YahangWu"><img src="https://avatars.githubusercontent.com/u/12295975?v=4?s=100" width="100px;" alt=""/></a></td>
</tr>
</table>
+4 -4
View File
@@ -10,16 +10,16 @@
<input type="search" id="search-input" placeholder="{{__('menu.search')}}...">
</div>
</nav>
<div id="lang-select-wrap" class="main-nav-link">
<label><i class="icon-network"></i>{{ lang_name(page.lang) }}</label>
<div class="main-nav-link icon-nav-link">
<label><i class="icon-network"></i> <span class="icon-nav-title">{{ lang_name(page.lang) }}</span></label>
<select id="lang-select" data-canonical="{{ canonical_path_for_nav() }}">
{% for lang in site.data.languages %}
<option value="{{ loop.key }}"{% if page.lang === loop.key %} selected{% endif %}>{{ lang_name(loop.key) }}</option>
{% endfor %}
</select>
</div>
<a href="https://github.com/{{ config.github }}" id="github-link-wrap" class="main-nav-link"><i class="icon-github"></i>Github</a>
<a href="https://opencollective.com/liquidjs" class="main-nav-link"><i class="icon-opencollective"></i>Support</a>
<a href="https://github.com/{{ config.github }}" class="main-nav-link icon-nav-link"><i class="icon-github"></i> <span class="icon-nav-title">Github</span></a>
<a href="https://opencollective.com/liquidjs" class="main-nav-link icon-nav-link"><i class="icon-opencollective"></i> <span class="icon-nav-title">Support</span></a>
<a id="mobile-nav-toggle">
<span class="mobile-nav-toggle-bar"></span>
<span class="mobile-nav-toggle-bar"></span>
+11 -10
View File
@@ -2,9 +2,6 @@
<div id="mobile-nav-inner">
<ul id="mobile-nav-list">
{{ header_menu('mobile-nav') }}
<li class="mobile-nav-item">
<a href="https://github.com/{{ config.github }}" class="mobile-nav-link" rel="external" target="_blank">GitHub</a>
</li>
</ul>
{% if page.layout == 'page' %}
<div class="mobile-sidebar-list">
@@ -12,12 +9,16 @@
</div>
{% endif %}
</div>
<div id="mobile-lang-select-wrap">
<span id="mobile-lang-select-label"><i class="icon-network"></i><span>{{ lang_name(page.lang) }}</span></span>
<select id="mobile-lang-select" data-canonical="{{ canonical_path_for_nav() }}">
{% for lang in site.data.languages %}
<option value="{{ loop.key }}"{% if page.lang === loop.key %} selected{% endif %}>{{ lang_name(loop.key) }}</option>
{% endfor %}
</select>
<div id="mobile-button-list">
<a href="https://github.com/{{ config.github }}" class="mobile-nav-link" rel="external" target="_blank"><i class="icon-github"></i></a>
<a href="https://opencollective.com/liquidjs" class="mobile-nav-link" rel="external" target="_blank"><i class="icon-opencollective"></i></a>
<div id="mobile-lang-select-wrap" class="mobile-nav-link">
<label for="mobile-lang-select"><i class="icon-network"></i></label>
<select id="mobile-lang-select" data-canonical="{{ canonical_path_for_nav() }}">
{% for lang in site.data.languages %}
<option value="{{ loop.key }}"{% if page.lang === loop.key %} selected{% endif %}>{{ lang_name(loop.key) }}</option>
{% endfor %}
</select>
</div>
</div>
</nav>
+7 -6
View File
@@ -29,7 +29,6 @@ logo-size = 36px
#main-nav
display: none
flex: 1 auto
padding-left: 20px
@media mq-normal
display: block
@@ -62,9 +61,7 @@ logo-size = 36px
opacity: 0.7
transition: opacity 0.2s, color 0.2s
display: inline-block
padding: 0 15px
i
padding-right: 8px
padding: 0 12px
&.active
opacity: 1
&:hover
@@ -72,10 +69,14 @@ logo-size = 36px
color: var(--color-link-hover)
text-decoration: none
#lang-select-wrap
#github-link-wrap
.icon-nav-link
display: none
position: relative
padding: 0 8px
.icon-nav-title
display: none
@media mq-small
display: inline
@media mq-normal
display: block
+9 -10
View File
@@ -78,7 +78,7 @@ lang-select-height = 40px
color: #fff
text-decoration: none
display: block
padding: 10px 15px
padding: 12px 16px
line-height: 1
white-space: nowrap
overflow: hidden
@@ -96,22 +96,21 @@ lang-select-height = 40px
overflow: hidden
text-overflow: ellipsis
#mobile-lang-select-wrap
#mobile-button-list
position: absolute
bottom: 0
left: 0
width: 100%
background: var(--color-navy)
border-top: 1px solid #444
padding: 12px 16px
.mobile-nav-link
display: inline-block
padding: 0
margin-right: 16px
#mobile-lang-select-label
line-height: lang-select-height
color: #fff
padding: 10px 15px
i
opacity: 0.7
span
padding-left: 8px
#mobile-lang-select-wrap
position: relative
#mobile-lang-select
-webkit-appearance: menulist-button
+3 -2
View File
@@ -21,11 +21,11 @@ vendor-prefixes = webkit moz ms official
--color-default: #d1d2d4
--color-default-invert: #444
--color-gray: invert(#999)
--color-border: invert(#e3e3e3)
--color-border: #6a6a6a
--color-backgrond: #fff
--color-navy: hsl(210, 25%, 12%)
--color-news-bg: darken(hsl(210, 25%, 12%), 5%)
--color-navy-lighter: lighten(hsl(210, 25%, 12%), 10%)
--color-navy-lighter: lighten(hsl(210, 25%, 12%), 7%)
--color-content-bg: #1c1c1c
--color-link: #0e83cd
--color-link-hover: lighten(#0e83cd, 10%)
@@ -48,4 +48,5 @@ mobile-nav-width = 260px
// Media queries
mq-mobile = "screen and (max-width: 768px)"
mq-normal = "screen and (min-width: 769px)"
mq-small = "screen and (min-width: 992px)"
mq-tablet = "screen and (min-width: 480px)"
Vendored
+1 -1
View File
@@ -1,5 +1,5 @@
declare module 'rollup-plugin-uglify' {
export function uglify (): any
export function uglify (options?: any): any
}
declare module 'rollup-plugin-replace' {
export default function replace(options: any): any
+22660 -3451
View File
File diff suppressed because it is too large Load Diff
+25 -6
View File
@@ -1,6 +1,6 @@
{
"name": "liquidjs",
"version": "9.34.1",
"version": "9.39.0",
"description": "A simple, expressive and safe Shopify / Github Pages compatible template engine in pure JavaScript.",
"main": "dist/liquid.node.cjs.js",
"module": "dist/liquid.node.esm.js",
@@ -91,16 +91,16 @@
"eslint-plugin-standard": "^4.0.0",
"express": "^4.16.4",
"husky": "^4.2.5",
"jsdom": "^13.2.0",
"jsdom": "^16.5.0",
"mocha": "^9.0.1",
"nyc": "^15.1.0",
"regenerator-runtime": "^0.12.1",
"rollup": "^2.61.0",
"rollup-plugin-replace": "^2.1.0",
"rollup-plugin-typescript2": "^0.31.1",
"rollup-plugin-uglify": "^5.0.2",
"rollup-plugin-uglify": "^6.0.4",
"rollup-plugin-version-injector": "^1.3.3",
"semantic-release": "^17.2.3",
"semantic-release": "^19.0.3",
"sinon": "^7.5.0",
"sinon-chai": "^3.3.0",
"supertest": "^3.4.2",
@@ -128,7 +128,25 @@
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}
],
"@semantic-release/github"
[
"@semantic-release/github",
{
"assets": [
{
"path": "dist/*.umd.js",
"label": "liquid.js"
},
{
"path": "dist/*.min.js",
"label": "liquid.min.js"
},
{
"path": "dist/*.min.js.map",
"label": "liquid.min.js.map"
}
]
}
]
]
},
"nyc": {
@@ -141,5 +159,6 @@
"pre-commit": "npm run check",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
}
},
"dependencies": {}
}
+20 -10
View File
@@ -37,16 +37,21 @@ const versionInjection = versionInjector({
exclude: []
})
const input = './src/liquid.ts'
const replaceFS = {
const browserFS = {
include: './src/liquid-options.ts',
delimiters: ['', ''],
'./fs/node': './fs/browser'
}
const replaceStream = {
const browserStream = {
include: './src/render/render.ts',
delimiters: ['', ''],
'../emitters/streamed-emitter': '../emitters/streamed-emitter-browser'
}
const esmRequire = {
include: './src/fs/node.ts',
delimiters: ['', ''],
'./node-require': './node-require.mjs'
}
const nodeCjs = {
output: [{
@@ -67,7 +72,11 @@ const nodeEsm = {
banner
}],
external: ['path', 'fs'],
plugins: [versionInjection, typescript(tsconfig)],
plugins: [
versionInjection,
replace(esmRequire),
typescript(tsconfig)
],
treeshake,
input
}
@@ -81,8 +90,8 @@ const browserEsm = {
external: ['path', 'fs'],
plugins: [
versionInjection,
replace(replaceFS),
replace(replaceStream),
replace(browserFS),
replace(browserStream),
typescript(tsconfig)
],
treeshake,
@@ -99,8 +108,8 @@ const browserUmd = {
}],
plugins: [
versionInjection,
replace(replaceFS),
replace(replaceStream),
replace(browserFS),
replace(browserStream),
typescript(tsconfig)
],
treeshake,
@@ -112,12 +121,13 @@ const browserMin = {
file: 'dist/liquid.browser.min.js',
name: 'liquidjs',
format: 'umd',
sourcemap
sourcemap,
banner
}],
plugins: [
versionInjection,
replace(replaceFS),
replace(replaceStream),
replace(browserFS),
replace(browserStream),
typescript(tsconfig),
uglify()
],
+31 -12
View File
@@ -1,17 +1,18 @@
import { isArray, isNil, last as arrayLast } from '../../util/underscore'
import { argumentsToValue, toValue, stringify, caseInsensitiveCompare, isArray, isNil, last as arrayLast, hasOwnProperty } from '../../util/underscore'
import { toArray } from '../../util/collection'
import { isTruthy } from '../../render/boolean'
import { FilterImpl } from '../../template/filter/filter-impl'
import { Scope } from '../../context/scope'
import { isComparable } from '../../drop/comparable'
export const join = (v: any[], arg: string) => v.join(arg === undefined ? ' ' : arg)
export const last = (v: any) => isArray(v) ? arrayLast(v) : ''
export const first = (v: any) => isArray(v) ? v[0] : ''
export const reverse = (v: any[]) => [...v].reverse()
export const join = argumentsToValue((v: any[], arg: string) => toArray(v).join(arg === undefined ? ' ' : arg))
export const last = argumentsToValue((v: any) => isArray(v) ? arrayLast(v) : '')
export const first = argumentsToValue((v: any) => isArray(v) ? v[0] : '')
export const reverse = argumentsToValue((v: any[]) => [...toArray(v)].reverse())
export function sort<T> (this: FilterImpl, arr: T[], property?: string) {
const getValue = (obj: Scope) => property ? this.context.getFromScope(obj, property.split('.')) : obj
arr = toValue(arr)
const getValue = (obj: Scope) => property ? this.context.getFromScope(obj, stringify(property).split('.')) : obj
return [...toArray(arr)].sort((lhs, rhs) => {
lhs = getValue(lhs)
rhs = getValue(rhs)
@@ -19,28 +20,45 @@ export function sort<T> (this: FilterImpl, arr: T[], property?: string) {
})
}
export function sortNatural<T> (input: T[], property?: string) {
input = toValue(input)
const propertyString = stringify(property)
const compare = property === undefined
? caseInsensitiveCompare
: (lhs: T, rhs: T) => caseInsensitiveCompare(lhs[propertyString], rhs[propertyString])
return [...toArray(input)].sort(compare)
}
export const size = (v: string | any[]) => (v && v.length) || 0
export function map (this: FilterImpl, arr: Scope[], property: string) {
return toArray(arr).map(obj => this.context.getFromScope(obj, property.split('.')))
arr = toValue(arr)
return toArray(arr).map(obj => this.context.getFromScope(obj, stringify(property).split('.')))
}
export function compact<T> (this: FilterImpl, arr: T[]) {
return toArray(arr).filter(x => !isNil(x))
arr = toValue(arr)
return toArray(arr).filter(x => !isNil(toValue(x)))
}
export function concat<T1, T2> (v: T1[], arg: T2[] | T2): (T1 | T2)[] {
export function concat<T1, T2> (v: T1[], arg: T2[] = []): (T1 | T2)[] {
v = toValue(v)
arg = toArray(arg).map(v => toValue(v))
return toArray(v).concat(arg)
}
export function slice<T> (v: T[], begin: number, length = 1): T[] {
export function slice<T> (v: T[] | string, begin: number, length = 1): T[] | string {
v = toValue(v)
if (isNil(v)) return []
if (!isArray(v)) v = stringify(v)
begin = begin < 0 ? v.length + begin : begin
return v.slice(begin, begin + length)
}
export function where<T extends object> (this: FilterImpl, arr: T[], property: string, expected?: any): T[] {
arr = toValue(arr)
return toArray(arr).filter(obj => {
const value = this.context.getFromScope(obj, String(property).split('.'))
const value = this.context.getFromScope(obj, stringify(property).split('.'))
if (expected === undefined) return isTruthy(value, this.context)
if (isComparable(expected)) return expected.equals(value)
return value === expected
@@ -48,9 +66,10 @@ export function where<T extends object> (this: FilterImpl, arr: T[], property: s
}
export function uniq<T> (arr: T[]): T[] {
arr = toValue(arr)
const u = {}
return (arr || []).filter(val => {
if (u.hasOwnProperty(String(val))) return false
if (hasOwnProperty.call(u, String(val))) return false
u[String(val)] = true
return true
})
+3 -1
View File
@@ -1,12 +1,14 @@
import strftime from '../../util/strftime'
import { LiquidDate } from '../../util/liquid-date'
import { isString, isNumber } from '../../util/underscore'
import { toValue, stringify, isString, isNumber } from '../../util/underscore'
import { FilterImpl } from '../../template/filter/filter-impl'
import { TimezoneDate } from '../../util/timezone-date'
export function date (this: FilterImpl, v: string | Date, arg: string) {
const opts = this.context.opts
let date: LiquidDate
v = toValue(v)
arg = stringify(arg)
if (v === 'now' || v === 'today') {
date = new Date()
} else if (isNumber(v)) {
+4 -4
View File
@@ -20,17 +20,17 @@ export function escape (str: string) {
}
function unescape (str: string) {
return String(str).replace(/&(amp|lt|gt|#34|#39);/g, m => unescapeMap[m])
return stringify(str).replace(/&(amp|lt|gt|#34|#39);/g, m => unescapeMap[m])
}
export function escapeOnce (str: string) {
return escape(unescape(str))
return escape(unescape(stringify(str)))
}
export function newlineToBr (v: string) {
return v.replace(/\n/g, '<br />\n')
return stringify(v).replace(/\n/g, '<br />\n')
}
export function stripHtml (v: string) {
return v.replace(/<script.*?<\/script>|<!--.*?-->|<style.*?<\/style>|<.*?>/g, '')
return stringify(v).replace(/<script.*?<\/script>|<!--.*?-->|<style.*?<\/style>|<.*?>/g, '')
}
+1 -1
View File
@@ -3,5 +3,5 @@ export * from './math'
export * from './url'
export * from './array'
export * from './date'
export * from './object'
export * from './misc'
export * from './string'
+14 -20
View File
@@ -1,30 +1,24 @@
import { caseInsensitiveCompare } from '../../util/underscore'
import { toValue, argumentsToValue } from '../../util/underscore'
export const abs = Math.abs
export const atLeast = Math.max
export const atMost = Math.min
export const ceil = Math.ceil
export const dividedBy = (v: number, arg: number) => v / arg
export const floor = Math.floor
export const minus = (v: number, arg: number) => v - arg
export const modulo = (v: number, arg: number) => v % arg
export const times = (v: number, arg: number) => v * arg
export const abs = argumentsToValue(Math.abs)
export const atLeast = argumentsToValue(Math.max)
export const atMost = argumentsToValue(Math.min)
export const ceil = argumentsToValue(Math.ceil)
export const dividedBy = 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 function round (v: number, arg = 0) {
v = toValue(v)
arg = toValue(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)
}
export function sortNatural (input: any[], property?: string) {
if (!input || !input.sort) return []
if (property !== undefined) {
return [...input].sort(
(lhs, rhs) => caseInsensitiveCompare(lhs[property], rhs[property])
)
}
return [...input].sort(caseInsensitiveCompare)
}
@@ -1,10 +1,10 @@
import { isFalsy } from '../../render/boolean'
import { isArray, isString, toValue } from '../../util/underscore'
import { identify, isArray, isString, toValue } from '../../util/underscore'
import { FilterImpl } from '../../template/filter/filter-impl'
export function Default<T1 extends boolean, T2> (this: FilterImpl, value: T1, defaultValue: T2, ...args: Array<[string, any]>): T1 | T2 {
if (isArray(value) || isString(value)) return value.length ? value : defaultValue
value = toValue(value)
if (isArray(value) || isString(value)) return value.length ? value : defaultValue
if (value === false && (new Map(args)).get('allow_false')) return false as T1
return isFalsy(value, this.context) ? defaultValue : value
}
@@ -12,3 +12,5 @@ export function Default<T1 extends boolean, T2> (this: FilterImpl, value: T1, de
export function json (value: any) {
return JSON.stringify(value)
}
export const raw = identify
+25 -7
View File
@@ -3,7 +3,7 @@
*
* * prefer stringify() to String() since `undefined`, `null` should eval ''
*/
import { stringify } from '../../util/underscore'
import { escapeRegExp, stringify } from '../../util/underscore'
import { assert } from '../../util/assert'
export function append (v: string, arg: string) {
@@ -16,7 +16,11 @@ export function prepend (v: string, arg: string) {
return stringify(arg) + stringify(v)
}
export function lstrip (v: string) {
export function lstrip (v: string, chars?: string) {
if (chars) {
chars = escapeRegExp(stringify(chars))
return stringify(v).replace(new RegExp(`^[${chars}]+`, 'g'), '')
}
return stringify(v).replace(/^\s+/, '')
}
@@ -36,15 +40,29 @@ export function removeFirst (v: string, l: string) {
return stringify(v).replace(String(l), '')
}
export function rstrip (str: string) {
export function rstrip (str: string, chars?: string) {
if (chars) {
chars = escapeRegExp(stringify(chars))
return stringify(str).replace(new RegExp(`[${chars}]+$`, 'g'), '')
}
return stringify(str).replace(/\s+$/, '')
}
export function split (v: string, arg: string) {
return stringify(v).split(String(arg))
const arr = stringify(v).split(String(arg))
// align to ruby split, which is the behavior of shopify/liquid
// see: https://ruby-doc.org/core-2.4.0/String.html#method-i-split
while (arr.length && arr[arr.length - 1] === '') arr.pop()
return arr
}
export function strip (v: string) {
export function strip (v: string, chars?: string) {
if (chars) {
chars = escapeRegExp(stringify(chars))
return stringify(v)
.replace(new RegExp(`^[${chars}]+`, 'g'), '')
.replace(new RegExp(`[${chars}]+$`, 'g'), '')
}
return stringify(v).trim()
}
@@ -68,11 +86,11 @@ export function replaceFirst (v: string, arg1: string, arg2: string) {
export function truncate (v: string, l = 50, o = '...') {
v = stringify(v)
if (v.length <= l) return v
return v.substr(0, l - o.length) + o
return v.substring(0, l - o.length) + o
}
export function truncatewords (v: string, l = 15, o = '...') {
const arr = v.split(/\s+/)
const arr = stringify(v).split(/\s+/)
let ret = arr.slice(0, l).join(' ')
if (arr.length >= l) ret += o
return ret
+4 -2
View File
@@ -1,2 +1,4 @@
export const urlDecode = (x: string) => x.split('+').map(decodeURIComponent).join(' ')
export const urlEncode = (x: string) => x.split(' ').map(encodeURIComponent).join('+')
import { stringify } from '../../util/underscore'
export const urlDecode = (x: string) => stringify(x).split('+').map(decodeURIComponent).join(' ')
export const urlEncode = (x: string) => stringify(x).split(' ').map(encodeURIComponent).join('+')
+2 -1
View File
@@ -18,10 +18,11 @@ import Break from './break'
import Continue from './continue'
import echo from './echo'
import liquid from './liquid'
import inlineComment from './inline-comment'
import { TagImplOptions } from '../../template/tag/tag-impl-options'
const tags: { [key: string]: TagImplOptions } = {
assign, 'for': For, capture, 'case': Case, comment, include, render, decrement, increment, cycle, 'if': If, layout, block, raw, tablerow, unless, 'break': Break, 'continue': Continue, echo, liquid
assign, 'for': For, capture, 'case': Case, comment, include, render, decrement, increment, cycle, 'if': If, layout, block, raw, tablerow, unless, 'break': Break, 'continue': Continue, echo, liquid, '#': inlineComment
}
export default tags
+11
View File
@@ -0,0 +1,11 @@
import { TagToken } from '../../tokens/tag-token'
import { TopLevelToken } from '../../tokens/toplevel-token'
import { TagImplOptions } from '../../template/tag/tag-impl-options'
export default {
parse: function (tagToken: TagToken, remainTokens: TopLevelToken[]) {
if (tagToken.args.search(/\n\s*[^#\s]/g) !== -1) {
throw new Error('every line of an inline comment must start with a \'#\' character')
}
}
} as TagImplOptions
+4
View File
@@ -1,5 +1,9 @@
import type { Template } from '../template/template'
export interface Cache<T> {
write (key: string, value: T): void | Promise<void>;
read (key: string): T | undefined | Promise<T | undefined>;
remove (key: string): void | Promise<void>;
}
export type LiquidCache = Cache<Template[] | Promise<Template[]>>
+14 -14
View File
@@ -5,17 +5,19 @@ import { Scope } from './scope'
import { isArray, isNil, isString, isFunction, toLiquid } from '../util/underscore'
import { InternalUndefinedVariableError } from '../util/error'
type PropertyKey = string | number;
export class Context {
/**
* insert a Context-level empty scope,
* for tags like {% capture %} {% assign %} to operate
* for tags like `{% capture %}` `{% assign %}` to operate
*/
private scopes: Scope[] = [{}]
private registers = {}
/**
* user passed in scope
* {% increment %}, {% decrement %} changes this scope,
* whereas {% capture %}, {% assign %} only hide this scope
* `{% increment %}`, `{% decrement %}` changes this scope,
* whereas `{% capture %}`, `{% assign %}` only hide this scope
*/
public environments: Scope
/**
@@ -54,11 +56,11 @@ export class Context {
return [this.globals, this.environments, ...this.scopes]
.reduce((ctx, val) => __assign(ctx, val), {})
}
public get (paths: string[]) {
public get (paths: PropertyKey[]) {
const scope = this.findScope(paths[0])
return this.getFromScope(scope, paths)
}
public getFromScope (scope: object, paths: string[] | string) {
public getFromScope (scope: object, paths: PropertyKey[] | string) {
if (isString(paths)) paths = paths.split('.')
return paths.reduce((scope, path, i) => {
scope = readProperty(scope, path, this.opts.ownPropertyOnly)
@@ -77,7 +79,7 @@ export class Context {
public bottom () {
return this.scopes[0]
}
private findScope (key: string) {
private findScope (key: string | number) {
for (let i = this.scopes.length - 1; i >= 0; i--) {
const candidate = this.scopes[i]
if (key in candidate) return candidate
@@ -87,21 +89,19 @@ export class Context {
}
}
export function readProperty (obj: Scope, key: string, ownPropertyOnly: boolean) {
export function readProperty (obj: Scope, key: PropertyKey, ownPropertyOnly: boolean) {
if (isNil(obj)) return obj
obj = toLiquid(obj)
if (isArray(obj) && key < 0) return obj[obj.length + +key]
const jsProperty = readJSProperty(obj, key, ownPropertyOnly)
if (jsProperty === undefined && obj instanceof Drop) return obj.liquidMethodMissing(key)
if (isFunction(jsProperty)) return jsProperty.call(obj)
if (obj instanceof Drop) {
if (obj.hasOwnProperty(key)) return obj[key]
return obj.liquidMethodMissing(key)
}
if (key === 'size') return readSize(obj)
if (key === 'first') return readFirst(obj)
if (key === 'last') return readLast(obj)
else if (key === 'first') return readFirst(obj)
else if (key === 'last') return readLast(obj)
return jsProperty
}
export function readJSProperty (obj: Scope, key: string, ownPropertyOnly: boolean) {
export function readJSProperty (obj: Scope, key: PropertyKey, ownPropertyOnly: boolean) {
if (ownPropertyOnly && !Object.hasOwnProperty.call(obj, key)) return undefined
return obj[key]
}
+1 -5
View File
@@ -1,9 +1,5 @@
export abstract class Drop {
public valueOf (): any {
return undefined
}
public liquidMethodMissing (key: string): Promise<string | undefined> | string | undefined {
public liquidMethodMissing (key: string | number): Promise<string | undefined> | string | undefined {
return undefined
}
}
+2 -2
View File
@@ -1,10 +1,10 @@
import { stringify } from '../util/underscore'
import { Emitter } from './emitter'
import { PassThrough } from 'stream'
export class StreamedEmitter implements Emitter {
public buffer = '';
// eslint-disable-next-line @typescript-eslint/no-var-requires
public stream: NodeJS.ReadWriteStream = new (require('stream').PassThrough)()
public stream: NodeJS.ReadWriteStream = new PassThrough()
public write (html: any) {
this.stream.write(stringify(html))
}
+10
View File
@@ -0,0 +1,10 @@
import { createRequire } from 'module'
export function requireResolve (file) {
/**
* createRequire() can throw,
* when import.meta.url not begin with "file://".
*/
const require = createRequire(import.meta.url)
return require.resolve(file)
}
+1
View File
@@ -0,0 +1 @@
export declare function requireResolve(file: string): string;
+1
View File
@@ -0,0 +1 @@
export const requireResolve = require.resolve
+2 -1
View File
@@ -1,6 +1,7 @@
import * as _ from '../util/underscore'
import { sep, resolve as nodeResolve, extname, dirname as nodeDirname } from 'path'
import { stat, statSync, readFile as nodeReadFile, readFileSync as nodeReadFileSync } from 'fs'
import { requireResolve } from './node-require'
const statAsync = _.promisify(stat)
const readFileAsync = _.promisify<string, string, string>(nodeReadFile)
@@ -33,7 +34,7 @@ export function resolve (root: string, file: string, ext: string) {
}
export function fallback (file: string) {
try {
return require.resolve(file)
return requireResolve(file)
} catch (e) {}
}
export function dirname (filepath: string) {
+33 -10
View File
@@ -1,12 +1,20 @@
import * as _ from './util/underscore'
import { Template } from './template/template'
import { Cache } from './cache/cache'
import { snakeCase, forOwn, isArray, isString, isFunction } from './util/underscore'
import { LiquidCache } from './cache/cache'
import { LRU } from './cache/lru'
import { FS } from './fs/fs'
import * as fs from './fs/node'
import { defaultOperators, Operators } from './render/operator'
import { createTrie, Trie } from './util/operator-trie'
import { Thenable } from './util/async'
import * as builtinFilters from './builtin/filters'
import { assert, FilterImplOptions } from './types'
const filters = new Map()
forOwn(builtinFilters, (conf: FilterImplOptions, name: string) => {
filters.set(snakeCase(name), conf)
})
type OutputEscape = (value: any) => string
type OutputEscapeOption = 'escape' | 'json' | OutputEscape
export interface LiquidOptions {
/** A directory or an array of directories from where to resolve layout and include templates, and the filename passed to `.renderFile()`. If it's an array, the files are looked up in the order they occur in the array. Defaults to `["."]` */
@@ -22,7 +30,7 @@ export interface LiquidOptions {
/** Add a extname (if filepath doesn't include one) before template file lookup. Eg: setting to `".html"` will allow including file by basename. Defaults to `""`. */
extname?: string;
/** Whether or not to cache resolved templates. Defaults to `false`. */
cache?: boolean | number | Cache<Thenable<Template[]>>;
cache?: boolean | number | LiquidCache;
/** Use Javascript Truthiness. Defaults to `false`. */
jsTruthy?: boolean;
/** If set, treat the `filepath` parameter in `{%include filepath %}` and `{%layout filepath%}` as a variable, otherwise as a literal value. Defaults to `true`. */
@@ -63,6 +71,8 @@ export interface LiquidOptions {
globals?: object;
/** Whether or not to keep value type when writing the Output, not working for streamed rendering. Defaults to `false`. */
keepOutputType?: boolean;
/** Default escape filter applied to output values, when set, you'll have to add `| raw` for values don't need to be escaped. Defaults to `undefined`. */
outputEscape?: OutputEscapeOption;
/** An object of operators for conditional statements. Defaults to the regular Liquid operators. */
operators?: Operators;
/** Respect parameter order when using filters like "for ... reversed limit", Defaults to `false`. */
@@ -92,7 +102,8 @@ interface NormalizedOptions extends LiquidOptions {
root?: string[];
partials?: string[];
layouts?: string[];
cache?: Cache<Thenable<Template[]>>;
cache?: LiquidCache;
outputEscape?: OutputEscape;
operatorsTrie?: Trie;
}
@@ -103,7 +114,7 @@ export interface NormalizedFullOptions extends NormalizedOptions {
relativeReference: boolean;
jekyllInclude: boolean;
extname: string;
cache: undefined | Cache<Thenable<Template[]>>;
cache?: LiquidCache;
jsTruthy: boolean;
dynamicPartials: boolean;
fs: FS;
@@ -167,7 +178,7 @@ export function normalize (options: LiquidOptions): NormalizedFullOptions {
if (!options.hasOwnProperty('layouts')) options.layouts = options.root
}
if (options.hasOwnProperty('cache')) {
let cache: Cache<Thenable<Template[]>> | undefined
let cache: LiquidCache | undefined
if (typeof options.cache === 'number') cache = options.cache > 0 ? new LRU(options.cache) : undefined
else if (typeof options.cache === 'object') cache = options.cache
else cache = options.cache ? new LRU(1024) : undefined
@@ -181,12 +192,24 @@ export function normalize (options: LiquidOptions): NormalizedFullOptions {
options.root = normalizeDirectoryList(options.root)
options.partials = normalizeDirectoryList(options.partials)
options.layouts = normalizeDirectoryList(options.layouts)
options.outputEscape = options.outputEscape && getOutputEscapeFunction(options.outputEscape)
return options as NormalizedFullOptions
}
function getOutputEscapeFunction (nameOrFunction: OutputEscapeOption) {
if (isString(nameOrFunction)) {
const filterImpl = filters.get(nameOrFunction)
assert(isFunction(filterImpl), `filter "${nameOrFunction}" not found`)
return filterImpl
} else {
assert(isFunction(nameOrFunction), '`outputEscape` need to be of type string or function')
return nameOrFunction
}
}
export function normalizeDirectoryList (value: any): string[] {
let list: string[] = []
if (_.isArray(value)) list = value
if (_.isString(value)) list = [value]
if (isArray(value)) list = value
if (isString(value)) list = [value]
return list
}
+5 -5
View File
@@ -12,7 +12,7 @@ import { TagMap } from './template/tag/tag-map'
import { FilterMap } from './template/filter/filter-map'
import { LiquidOptions, normalizeDirectoryList, NormalizedFullOptions, normalize, RenderOptions } from './liquid-options'
import { FilterImplOptions } from './template/filter/filter-impl-options'
import { toPromise, toValue } from './util/async'
import { toPromise, toValueSync } from './util/async'
export * from './util/error'
export * from './types'
@@ -47,7 +47,7 @@ export class Liquid {
return toPromise(this._render(tpl, scope, { ...renderOptions, sync: false }))
}
public renderSync (tpl: Template[], scope?: object, renderOptions?: RenderOptions): any {
return toValue(this._render(tpl, scope, { ...renderOptions, sync: true }))
return toValueSync(this._render(tpl, scope, { ...renderOptions, sync: true }))
}
public renderToNodeStream (tpl: Template[], scope?: object, renderOptions: RenderOptions = {}): NodeJS.ReadableStream {
const ctx = new Context(scope, this.options, renderOptions)
@@ -62,7 +62,7 @@ export class Liquid {
return toPromise(this._parseAndRender(html, scope, { ...renderOptions, sync: false }))
}
public parseAndRenderSync (html: string, scope?: object, renderOptions?: RenderOptions): any {
return toValue(this._parseAndRender(html, scope, { ...renderOptions, sync: true }))
return toValueSync(this._parseAndRender(html, scope, { ...renderOptions, sync: true }))
}
public _parsePartialFile (file: string, sync?: boolean, currentFile?: string) {
@@ -75,7 +75,7 @@ export class Liquid {
return toPromise<Template[]>(this.parser.parseFile(file, false))
}
public parseFileSync (file: string): Template[] {
return toValue<Template[]>(this.parser.parseFile(file, true))
return toValueSync<Template[]>(this.parser.parseFile(file, true))
}
public async renderFile (file: string, ctx?: object, renderOptions?: RenderOptions) {
const templates = await this.parseFile(file)
@@ -98,7 +98,7 @@ export class Liquid {
return toPromise(this._evalValue(str, ctx))
}
public evalValueSync (str: string, ctx: Context): any {
return toValue(this._evalValue(str, ctx))
return toValueSync(this._evalValue(str, ctx))
}
public registerFilter (name: string, filter: FilterImplOptions) {
+13 -16
View File
@@ -8,17 +8,17 @@ import { Output } from '../template/output'
import { HTML } from '../template/html'
import { Template } from '../template/template'
import { TopLevelToken } from '../tokens/toplevel-token'
import { Cache } from '../cache/cache'
import { LiquidCache } from '../cache/cache'
import { Loader, LookupType } from '../fs/loader'
import { toPromise } from '../util/async'
import { FS } from '../fs/fs'
import { toThenable, Thenable } from '../util/async'
export default class Parser {
public parseFile: (file: string, sync?: boolean, type?: LookupType, currentFile?: string) => Generator<unknown, Template[], Template[] | string>
private liquid: Liquid
private fs: FS
private cache: Cache<Thenable<Template[]>> | undefined
private cache?: LiquidCache
private loader: Loader
public constructor (liquid: Liquid) {
@@ -58,21 +58,18 @@ export default class Parser {
return new ParseStream(tokens, (token, tokens) => this.parseToken(token, tokens))
}
private * _parseFileCached (file: string, sync?: boolean, type: LookupType = LookupType.Root, currentFile?: string): Generator<unknown, Template[], Template[]> {
const key = this.loader.shouldLoadRelative(file)
? currentFile + ',' + file
: type + ':' + file
const tpls = yield this.cache!.read(key)
const cache = this.cache!
const key = this.loader.shouldLoadRelative(file) ? currentFile + ',' + file : type + ':' + file
const tpls = yield cache.read(key)
if (tpls) return tpls
const task = toThenable(this._parseFile(file, sync, type, currentFile))
this.cache!.write(key, task)
try {
return yield task
} catch (e) {
// remove cached task if failed
this.cache!.remove(key)
}
return []
const task = this._parseFile(file, sync, type, currentFile)
// sync mode: exec the task and cache the result
// async mode: cache the task before exec
const taskOrTpl = sync ? yield task : toPromise(task)
cache.write(key, taskOrTpl as any)
// note: concurrent tasks will be reused, cache for failed task is removed until its end
try { return yield taskOrTpl } catch (err) { cache.remove(key); throw err }
}
private * _parseFile (file: string, sync?: boolean, type: LookupType = LookupType.Root, currentFile?: string): Generator<unknown, Template[], string> {
const filepath = yield this.loader.lookup(file, type, sync, currentFile)
+10 -3
View File
@@ -89,7 +89,8 @@ export class Tokenizer {
++this.p
const arg = this.readFilterArg()
arg && args.push(arg)
while (this.p < this.N && this.peek() !== ',' && this.peek() !== '|') ++this.p
this.skipBlank()
assert(this.end() || this.peek() === ',' || this.peek() === '|', () => `unexpected character ${this.snapshot()}`)
} while (this.peek() === ',')
}
return new FilterToken(name.getText(), args, this.input, begin, this.p, this.file)
@@ -206,8 +207,7 @@ export class Tokenizer {
const begin = this.p
let end = this.N
if (this.readToDelimiter('\n') !== -1) end = this.p
const token = new LiquidTagToken(input, begin, end, options, file)
return token
return new LiquidTagToken(input, begin, end, options, file)
}
mkError (msg: string, begin: number) {
@@ -233,6 +233,13 @@ export class Tokenizer {
return new IdentifierToken(this.input, begin, this.p, this.file)
}
readTagName (): string {
this.skipBlank()
// Handle inline comment tags
if (this.input[this.p] === '#') return this.input.slice(this.p, ++this.p)
return this.readIdentifier().getText()
}
readHashes (jekyllStyle?: boolean) {
const hashes = []
while (true) {
+3 -3
View File
@@ -25,9 +25,9 @@ export class Expression {
const operands: any[] = []
for (const token of this.postfix) {
if (TypeGuards.isOperatorToken(token)) {
const r = yield operands.pop()
const l = yield operands.pop()
const result = evalOperatorToken(ctx.opts.operators, token, l, r, ctx)
const r = operands.pop()
const l = operands.pop()
const result = yield evalOperatorToken(ctx.opts.operators, token, l, r, ctx)
operands.push(result)
} else {
operands.push(yield evalToken(token, ctx, lenient && this.postfix.length === 1))
+3 -1
View File
@@ -1,6 +1,6 @@
import { isComparable } from '../drop/comparable'
import { Context } from '../context/context'
import { isFunction } from '../util/underscore'
import { isFunction, toValue } from '../util/underscore'
import { isTruthy } from '../render/boolean'
export interface Operators {
@@ -39,6 +39,8 @@ export const defaultOperators: Operators = {
return l <= r
},
'contains': (l: any, r: any) => {
l = toValue(l)
r = toValue(r)
return l && isFunction(l.indexOf) ? l.indexOf(r) > -1 : false
},
'and': (l: any, r: any, ctx: Context) => isTruthy(l, ctx) && isTruthy(r, ctx),
+2 -2
View File
@@ -4,13 +4,13 @@ import { Template } from '../template/template'
import { Emitter } from '../emitters/emitter'
import { SimpleEmitter } from '../emitters/simple-emitter'
import { StreamedEmitter } from '../emitters/streamed-emitter'
import { toThenable } from '../util/async'
import { toPromise } from '../util/async'
import { KeepingTypeEmitter } from '../emitters/keeping-type-emitter'
export class Render {
public renderTemplatesToNodeStream (templates: Template[], ctx: Context): NodeJS.ReadableStream {
const emitter = new StreamedEmitter()
Promise.resolve().then(() => toThenable(this.renderTemplates(templates, ctx, emitter)))
Promise.resolve().then(() => toPromise(this.renderTemplates(templates, ctx, emitter)))
.then(() => emitter.end(), err => emitter.error(err))
return emitter.stream
}
+8
View File
@@ -5,12 +5,20 @@ import { Context } from '../context/context'
import { Emitter } from '../emitters/emitter'
import { OutputToken } from '../tokens/output-token'
import { Liquid } from '../liquid'
import { Filter } from './filter/filter'
export class Output extends TemplateImpl<OutputToken> implements Template {
private value: Value
public constructor (token: OutputToken, liquid: Liquid) {
super(token)
this.value = new Value(token.content, liquid)
const filters = this.value.filters
const outputEscape = liquid.options.outputEscape
if (filters.length && filters[filters.length - 1].name === 'raw') {
filters.pop()
} else if (outputEscape) {
filters.push(new Filter(toString.call(outputEscape), outputEscape, [], liquid))
}
}
public * render (ctx: Context, emitter: Emitter): IterableIterator<unknown> {
const val = yield this.value.value(ctx, false)
+1 -1
View File
@@ -23,7 +23,7 @@ export class LiquidTagToken extends DelimitedToken {
this.args = ''
} else {
const tokenizer = new Tokenizer(this.content, options.operatorsTrie)
this.name = tokenizer.readIdentifier().getText()
this.name = tokenizer.readTagName()
if (!this.name) throw new TokenizationError(`illegal liquid tag syntax`, this)
tokenizer.skipBlank()
+1 -1
View File
@@ -19,7 +19,7 @@ export class TagToken extends DelimitedToken {
super(TokenKind.Tag, value, input, begin, end, trimTagLeft, trimTagRight, file)
const tokenizer = new Tokenizer(this.content, options.operatorsTrie)
this.name = tokenizer.readIdentifier().getText()
this.name = tokenizer.readTagName()
if (!this.name) throw new TokenizationError(`illegal tag syntax`, this)
tokenizer.skipBlank()
+3
View File
@@ -25,3 +25,6 @@ export { toPromise, toThenable } from './util/async'
export { defaultOperators, Operators } from './render/operator'
export { createTrie, Trie } from './util/operator-trie'
export { toValue } from './util/underscore'
export { TimezoneDate } from './util/timezone-date'
export * as filters from './builtin/filters'
export * as tags from './builtin/tags'
+39 -72
View File
@@ -1,81 +1,48 @@
import { isFunction } from './underscore'
import { isPromise, isIterator } from './underscore'
type resolver = (x?: any) => any
export interface Thenable<T> {
then (resolve: resolver, reject?: resolver): Thenable<T>;
catch (reject: resolver): Thenable<T>;
}
function createResolvedThenable<T> (value: T): Thenable<T> {
const ret = {
then: (resolve: resolver) => resolve(value),
catch: () => ret
}
return ret
}
function createRejectedThenable<T> (err: Error): Thenable<T> {
const ret = {
then: (resolve: resolver, reject?: resolver) => {
if (reject) return reject(err)
return ret
},
catch: (reject: resolver) => reject(err)
}
return ret
}
function isThenable<T> (val: any): val is Thenable<T> {
return val && isFunction(val.then)
}
function isAsyncIterator (val: any): val is IterableIterator<any> {
return val && isFunction(val.next) && isFunction(val.throw) && isFunction(val.return)
}
// convert an async iterator to a thenable (Promise compatible)
export function toThenable<T> (val: IteratorResult<unknown, T> | Thenable<T> | any): Thenable<T> {
if (isThenable(val)) return val
if (isAsyncIterator(val)) return reduce()
return createResolvedThenable(val)
function reduce<T> (prev?: T): Thenable<T> {
let state
// convert an async iterator to a Promise
export async function toPromise<T> (val: Generator<unknown, T, unknown> | Promise<T> | T): Promise<T> {
if (!isIterator(val)) return val
let value: unknown
let done = false
let next = 'next'
do {
const state = val[next](value)
done = state.done
value = state.value
next = 'next'
try {
state = val.next(prev)
if (isIterator(value)) value = toPromise(value)
if (isPromise(value)) value = await value
} catch (err) {
return createRejectedThenable(err as Error)
next = 'throw'
value = err
}
} while (!done)
return value as T
}
if (state.done) return createResolvedThenable(state.value)
return toThenable(state.value!).then(reduce, err => {
let state
// convert an async iterator to a value in a synchronous manner
export function toValueSync<T> (val: Generator<unknown, T, unknown> | T): T {
if (!isIterator(val)) return val
let value: any
let done = false
let next = 'next'
do {
const state = val[next](value)
done = state.done
value = state.value
next = 'next'
if (isIterator(value)) {
try {
state = val.throw!(err)
} catch (e) {
return createRejectedThenable(e as Error)
value = toValueSync(value)
} catch (err) {
next = 'throw'
value = err
}
if (state.done) return createResolvedThenable(state.value)
return reduce(state.value)
})
}
}
} while (!done)
return value
}
export function toPromise<T> (val: Generator<unknown, T, unknown> | Thenable<T> | T): Promise<T> {
return Promise.resolve(toThenable(val))
}
// get the value of async iterator in synchronous manner
export function toValue<T> (val: Generator<unknown, T, unknown> | Thenable<T> | T): T {
let ret: T
toThenable(val)
.then((x: any) => {
ret = x
return createResolvedThenable(ret)
})
.catch((err: Error) => {
throw err
})
return ret!
}
export const toThenable = toPromise
+4 -1
View File
@@ -1,13 +1,16 @@
import { isString, isObject, isArray } from './underscore'
import { isNil, isString, isObject, isArray, isIterable, toValue } from './underscore'
export function toEnumerable (val: any) {
val = toValue(val)
if (isArray(val)) return val
if (isString(val) && val.length > 0) return [val]
if (isIterable(val)) return Array.from(val)
if (isObject(val)) return Object.keys(val).map((key) => [key, val[key]])
return []
}
export function toArray (val: any) {
if (isNil(val)) return []
if (isArray(val)) return val
return [ val ]
}
+26 -4
View File
@@ -1,8 +1,10 @@
import { Drop } from '../drop/drop'
const toStr = Object.prototype.toString
export const toString = Object.prototype.toString
const toLowerCase = String.prototype.toLowerCase
export const hasOwnProperty = Object.hasOwnProperty
export function isString (value: any): value is string {
return typeof value === 'string'
}
@@ -12,6 +14,14 @@ export function isFunction (value: any): value is Function {
return typeof value === 'function'
}
export function isPromise<T> (val: any): val is Promise<T> {
return val && isFunction(val.then)
}
export function isIterator (val: any): val is IterableIterator<any> {
return val && isFunction(val.next) && isFunction(val.throw) && isFunction(val.return)
}
export function escapeRegex (str: string) {
return str.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')
}
@@ -37,7 +47,7 @@ export function stringify (value: any): string {
}
export function toValue (value: any): any {
return value instanceof Drop ? value.valueOf() : value
return (value instanceof Drop && isFunction(value.valueOf)) ? value.valueOf() : value
}
export function isNumber (value: any): value is number {
@@ -55,7 +65,11 @@ export function isNil (value: any): boolean {
export function isArray (value: any): value is any[] {
// be compatible with IE 8
return toStr.call(value) === '[object Array]'
return toString.call(value) === '[object Array]'
}
export function isIterable (value: any): value is Iterable<any> {
return isObject(value) && Symbol.iterator in value
}
/*
@@ -72,7 +86,7 @@ export function forOwn <T> (
) {
obj = obj || {}
for (const k in obj) {
if (Object.hasOwnProperty.call(obj, k)) {
if (hasOwnProperty.call(obj, k)) {
if (iteratee(obj[k], k, obj) === false) break
}
}
@@ -150,3 +164,11 @@ export function caseInsensitiveCompare (a: any, b: any) {
if (a > b) return 1
return 0
}
export function argumentsToValue<F extends (...args: any) => any> (fn: F) {
return (...args: Parameters<F>) => fn(...args.map(toValue))
}
export function escapeRegExp (text: string) {
return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')
}
+58 -1
View File
@@ -1,4 +1,4 @@
import { Liquid } from '../..'
import { Liquid, Drop } from '../..'
import { expect, use } from 'chai'
import * as chaiAsPromised from 'chai-as-promised'
import * as sinon from 'sinon'
@@ -199,4 +199,61 @@ describe('Issues', function () {
const html = engine.parseAndRenderSync('{{foo | size}}-{{bar.coo}}', { foo: 'foo', bar: Object.create({ coo: 'COO' }) })
expect(html).to.equal('3-')
})
it('#465 Liquidjs divided_by not compatible with Ruby/Shopify Liquid', () => {
const engine = new Liquid({ ownPropertyOnly: true })
const html = engine.parseAndRenderSync('{{ 5 | divided_by: 3, true }}')
expect(html).to.equal('1')
})
it('#479 url_encode throws on undefined value', async () => {
const engine = new Liquid({
strictVariables: false
})
const tpl = engine.parse('{{ v | url_encode }}')
const html = await engine.render(tpl, { v: undefined })
expect(html).to.equal('')
})
it('#481 filters that should not throw', async () => {
const engine = new Liquid()
const tpl = engine.parse(`
{{ foo | join }}
{{ foo | map: "k" }}
{{ foo | reverse }}
{{ foo | slice: 2 }}
{{ foo | newline_to_br }}
{{ foo | strip_html }}
{{ foo | truncatewords }}
{{ foo | concat | json }}
`)
const html = await engine.render(tpl, { foo: undefined })
expect(html.trim()).to.equal('[]')
})
it('#481 concat should always return an array', async () => {
const engine = new Liquid()
const html = await engine.parseAndRender(`{{ foo | concat | json }}`)
expect(html).to.equal('[]')
})
it('#486 Access array items from the right with negative indexes', async () => {
const engine = new Liquid()
const html = await engine.parseAndRender(`{% assign a = "x,y,z" | split: ',' -%}{{ a[-1] }} {{ a[-3] }} {{ a[-8] }}`)
expect(html).to.equal('z x ')
})
it('#492 contains operator does not support Drop', async () => {
class TemplateDrop extends Drop {
valueOf () { return 'product' }
}
const engine = new Liquid()
const ctx = { template: new TemplateDrop() }
const html = await engine.parseAndRender(`{% if template contains "product" %}contains{%endif%}`, ctx)
expect(html).to.equal('contains')
})
it('#513 should support large number of templates [async]', async () => {
const engine = new Liquid()
const html = await engine.parseAndRender(`{% for i in (1..10000) %}{{ i }}{% endfor %}`)
expect(html).to.have.lengthOf(38894)
})
it('#513 should support large number of templates [sync]', () => {
const engine = new Liquid()
const html = engine.parseAndRenderSync(`{% for i in (1..10000) %}{{ i }}{% endfor %}`)
expect(html).to.have.lengthOf(38894)
})
})
+92 -4
View File
@@ -28,10 +28,27 @@ describe('filters/array', function () {
return expect(render(src)).to.be.rejectedWith('unexpected token at "\\" and \\"", line:1, col:65')
})
})
it('should support split/last', function () {
const src = '{% assign my_array = "zebra, octopus, giraffe, tiger" | split: ", " %}' +
'{{ my_array|last }}'
return test(src, 'tiger')
describe('last', () => {
it('should support last', function () {
const src = '{{ arr | last }}'
const scope = { arr: ['zebra', 'octopus', 'giraffe', 'tiger'] }
return test(src, scope, 'tiger')
})
})
describe('split', () => {
it('should support split', function () {
const src = '{% assign my_array = "zebra, octopus, giraffe, tiger" | split: ", " %}' +
'{{ my_array|last }}'
return test(src, 'tiger')
})
it('should remove trailing empty strings', async () => {
const src = '{{ "zebra,octopus,,,," | split: "," | join: ", " }}'
return test(src, {}, 'zebra, octopus')
})
it('should return empty array for nil value', async () => {
await test('{{ notDefined | split: "," | size }}', {}, '0')
await test('{{ nil | split: "," | size }}', {}, '0')
})
})
describe('map', () => {
it('should support map', function () {
@@ -56,6 +73,28 @@ describe('filters/array', function () {
return test('{{posts | map: "category" | compact}}', { posts }, 'foobar')
})
})
describe('concat', () => {
it('should concat args value', async () => {
const scope = { val: ['hey'], arr: ['foo', 'bar'] }
await test('{{ val | concat: arr | join: "," }}', scope, 'hey,foo,bar')
})
it('should support undefined left value', async () => {
const scope = { arr: ['foo', 'bar'] }
await test('{{ notDefined | concat: arr | join: "," }}', scope, 'foo,bar')
})
it('should ignore nil left value', async () => {
const scope = { undefinedValue: undefined, nullValue: null, arr: ['foo', 'bar'] }
await test('{{ undefinedValue | concat: arr | join: "," }}', scope, 'foo,bar')
await test('{{ nullValue | concat: arr | join: "," }}', scope, 'foo,bar')
})
it('should ignore nil right value', async () => {
const scope = { nullValue: null }
await test('{{ nullValue | concat | join: "," }}', scope, '')
await test('{{ nullValue | concat: nil | join: "," }}', scope, '')
})
})
describe('reverse', function () {
it('should support reverse', () => test(
'{{ "Ground control to Major Tom." | split: "" | reverse | join: "" }}',
@@ -118,6 +157,7 @@ describe('filters/array', function () {
it('should slice substr by -3,2', () => test('{{ "Liquid" | slice: -3, 2 }}', 'ui'))
it('should slice substr by -2,2', () => test('{{ "abc" | slice: -2, 2 }}', 'bc'))
it('should support array', () => test('{{ "1,2,3,4" | split: "," | slice: 1,2 | join }}', '2 3'))
it('should return empty array for nil value', () => test('{{ nil | slice: 0 }}', ''))
})
describe('sort', function () {
it('should support sort', function () {
@@ -142,6 +182,54 @@ describe('filters/array', function () {
const arr = ['one', 'two', 'three', 'four', 'five']
return test('{{arr | sort}} {{arr}}', { arr }, 'fivefouronethreetwo onetwothreefourfive')
})
it('should return empty array for nil value', () => {
return test('{{notDefined | sort | size}}', {}, '0')
})
})
describe('sort_natural', function () {
it('should sort alphabetically', () => {
return test(
'{% assign my_array = "zebra, octopus, giraffe, Sally Snake" | split: ", " %}{{ my_array | sort_natural | join: ", " }}',
'giraffe, octopus, Sally Snake, zebra'
)
})
it('should sort with specified property', () => test(
'{{ students | sort_natural: "name" | map: "name" | join }}',
{ students: [{ name: 'bob' }, { name: 'alice' }, { name: 'carol' }] },
'alice bob carol'
))
it('should be stable', () => test(
'{{ students | sort_natural: "age" | map: "name" | join }}',
{ students: [{ name: 'bob', age: 1 }, { name: 'alice', age: 1 }, { name: 'carol', age: 1 }] },
'bob alice carol'
))
it('should be stable when it comes to undefined props', () => test(
'{{ students | sort_natural: "age" | map: "name" | join }}',
{ students: [{ name: 'bob' }, { name: 'alice', age: 2 }, { name: 'amber' }, { name: 'watson' }, { name: 'michael' }, { name: 'charlie' }] },
'alice bob amber watson michael charlie'
))
it('should tolerate undefined props', () => test(
'{{ students | sort_natural: "age" | map: "name" | join }}',
{ students: [{ name: 'bob' }, { name: 'alice', age: 2 }, { name: 'carol' }] },
'alice bob carol'
))
it('should tolerate non array', async () => {
await test(
'{{ students | sort_natural: "age" | map: "name" | join }}',
{ students: {} },
''
)
await test(
'{{ students | sort_natural: "age" | map: "name" | size }}',
{ students: {} },
'1'
)
})
it('should return empty array for nil value', () => test(
'{{ students | sort_natural: "age" | map: "name" | size }}',
{ students: undefined },
'0'
))
})
describe('uniq', function () {
it('should uniq string list', function () {
+2
View File
@@ -18,6 +18,8 @@ describe('filters/html', function () {
test('{{ "1 < 2 & 3" | escape_once }}', '1 &lt; 2 &amp; 3'))
it('should not escape twice',
() => test('{{ "1 &lt; 2 &amp; 3" | escape_once }}', '1 &lt; 2 &amp; 3'))
it('should escape nil value to empty string', () =>
test('{{ undefinedValue | escape_once }}', ''))
})
describe('newline_to_br', function () {
it('should support string_with_newlines', function () {
+2 -38
View File
@@ -25,6 +25,8 @@ describe('filters/math', function () {
it('should return 2 for 4,2', () => test('{{4 | divided_by: 2}}', '2'))
it('should return 4 for 16,4', () => test('{{16 | divided_by: 4}}', '4'))
it('should return 1 for 5,3', () => test('{{5 | divided_by: 3}}', (5 / 3).toString()))
it('should support integer arithmetic', () => test('{{5 | divided_by: 3, true}}', '1'))
it('should floor the result in integer arithmetic', () => test('{{ -5 | divided_by: 3, true}}', '-2'))
it('should convert string to number', () => test('{{"6" | divided_by: "3"}}', '2'))
})
describe('floor', function () {
@@ -60,44 +62,6 @@ describe('filters/math', function () {
it('should support variable', () => test('{{ 4 | plus: b }}', { b: 2 }, '6'))
})
describe('sort_natural', function () {
it('should sort alphabetically', () => {
return test(
'{% assign my_array = "zebra, octopus, giraffe, Sally Snake" | split: ", " %}{{ my_array | sort_natural | join: ", " }}',
'giraffe, octopus, Sally Snake, zebra'
)
})
it('should sort with specified property', () => test(
'{{ students | sort_natural: "name" | map: "name" | join }}',
{ students: [{ name: 'bob' }, { name: 'alice' }, { name: 'carol' }] },
'alice bob carol'
))
it('should be stable', () => test(
'{{ students | sort_natural: "age" | map: "name" | join }}',
{ students: [{ name: 'bob', age: 1 }, { name: 'alice', age: 1 }, { name: 'carol', age: 1 }] },
'bob alice carol'
))
it('should be stable when it comes to undefined props', () => test(
'{{ students | sort_natural: "age" | map: "name" | join }}',
{ students: [{ name: 'bob' }, { name: 'alice', age: 2 }, { name: 'amber' }, { name: 'watson' }, { name: 'michael' }, { name: 'charlie' }] },
'alice bob amber watson michael charlie'
))
it('should tolerate undefined props', () => test(
'{{ students | sort_natural: "age" | map: "name" | join }}',
{ students: [{ name: 'bob' }, { name: 'alice', age: 2 }, { name: 'carol' }] },
'alice bob carol'
))
it('should tolerate non array', () => test(
'{{ students | sort_natural: "age" | map: "name" | join }}',
{ students: {} },
''
))
it('should tolerate falsy input', () => test(
'{{ students | sort_natural: "age" | map: "name" | join }}',
{ students: undefined },
''
))
})
describe('round', function () {
it('should return "1" for 1.2', () => test('{{1.2|round}}', '1'))
it('should return "3" for 2.7', () => test('{{2.7|round}}', '3'))
@@ -1,5 +1,8 @@
import { Liquid } from '../../../../src/liquid'
import { expect } from 'chai'
import { expect, use } from 'chai'
import * as chaiAsPromised from 'chai-as-promised'
use(chaiAsPromised)
describe('filters/object', function () {
const liquid = new Liquid()
@@ -15,6 +18,10 @@ describe('filters/object', function () {
it('should output false when allow_false=true', async () => expect(await liquid.parseAndRender('{{false | default: true, allow_false: true}}')).to.equal('false'))
it('should output default without allow_false', async () => expect(await liquid.parseAndRender('{{false | default: true}}')).to.equal('true'))
it('should output default when allow_false=false', async () => expect(await liquid.parseAndRender('{{false | default: true, allow_false: false}}')).to.equal('true'))
it('should throw for additional args', () => {
const src = `{{ age | default: 'now' date: '%d'}}` // missing `|` before `date`
return expect(liquid.parseAndRender(src)).to.be.rejectedWith(/unexpected character "date: '%d'"/)
})
})
describe('json', function () {
it('should stringify string', async () => expect(await liquid.parseAndRender('{{"foo" | json}}')).to.equal('"foo"'))
+11 -6
View File
@@ -86,9 +86,10 @@ describe('filters/string', function () {
it('should support upcase', () => test('{{ "Parker Moore" | upcase }}', 'PARKER MOORE'))
it('should return empty for undefined', () => test('{{ foo | upcase }}', ''))
})
it('should support lstrip', function () {
it('should support lstrip', async () => {
const src = '{{ " So much room for activities! " | lstrip }}'
return test(src, 'So much room for activities! ')
await test(src, 'So much room for activities! ')
await test('{{ "foobarcoo" | lstrip: "fo" }}', 'barcoo')
})
it('should support prepend', function () {
return test('{% assign url = "liquidmarkup.com" %}' +
@@ -115,9 +116,10 @@ describe('filters/string', function () {
'{{ my_string | replace_first: "my", "your" }}',
'\nTake your protein pills and put my helmet on')
})
it('should support rstrip', function () {
return test('{{ " So much room for activities! " | rstrip }}',
it('should support rstrip', async () => {
await test('{{ " So much room for activities! " | rstrip }}',
' So much room for activities!')
await test('{{ "foobarcoo" | rstrip: "fco" }}', 'foobar')
})
it('should support split', function () {
return test('{% assign beatles = "John, Paul, George, Ringo" | split: ", " %}' +
@@ -126,9 +128,12 @@ describe('filters/string', function () {
'{% endfor %}',
'John Paul George Ringo ')
})
it('should support strip', function () {
return test('{{ " So much room for activities! " | strip }}',
it('should support strip', async () => {
await test('{{ " So much room for activities! " | strip }}',
'So much room for activities!')
await test('{{ " So much room for activities! " | strip: "So " }}',
'much room for activities!')
await test('{{ "&[]{}" | strip: "&[]{}" }}', '')
})
it('should support strip_newlines', function () {
return test('{% capture string_with_newlines %}\n' +
+5 -1
View File
@@ -6,7 +6,7 @@ use(chaiAsPromised)
describe('tags/assign', function () {
const liquid = new Liquid()
it('should throw when variable expression illegal', function () {
it('should throw when variable name illegal', function () {
const src = '{% assign / %}'
const ctx = {}
return expect(liquid.parseAndRender(src, ctx)).to.be.rejectedWith(/illegal/)
@@ -16,6 +16,10 @@ describe('tags/assign', function () {
const html = await liquid.parseAndRender(src)
return expect(html).to.equal('bar')
})
it('should throw when variable value illegal', async function () {
const src = '{% assign foo = “bar” %}'
return expect(liquid.parseAndRender(src)).to.be.rejectedWith(/unexpected token at "“bar”"/)
})
it('should support assign to a number', async function () {
const src = '{% assign foo=10086 %}{{foo}}'
const html = await liquid.parseAndRender(src)
+62 -1
View File
@@ -1,4 +1,4 @@
import { Liquid } from '../../../../src/liquid'
import { Drop, Liquid } from '../../../../src/liquid'
import { expect, use } from 'chai'
import * as chaiAsPromised from 'chai-as-promised'
import { Scope } from '../../../../src/context/scope'
@@ -304,4 +304,65 @@ describe('tags/for', function () {
return expect(html).to.equal('b')
})
})
describe('iterables', function () {
class MockIterable {
* [Symbol.iterator] () {
yield 'a'
yield 'b'
yield 'c'
}
}
class MockEmptyIterable {
* [Symbol.iterator] () {}
}
class MockIterableDrop extends Drop {
* [Symbol.iterator] () {
yield 'a'
yield 'b'
yield 'c'
}
toString () {
return 'MockIterableDrop'
}
}
it('should loop over iterable objects', function () {
const src = '{% for i in someIterable %}{{i}}{%endfor%}'
const html = liquid.parseAndRenderSync(src, { someIterable: new MockIterable() })
return expect(html).to.equal('abc')
})
it('should loop over iterable drops', function () {
const src = '{{ someDrop }}: {% for i in someDrop %}{{i}}{%endfor%}'
const html = liquid.parseAndRenderSync(src, { someDrop: new MockIterableDrop() })
return expect(html).to.equal('MockIterableDrop: abc')
})
it('should loop over iterable objects with a limit', function () {
const src = '{% for i in someIterable limit:2 %}{{i}}{%endfor%}'
const html = liquid.parseAndRenderSync(src, { someIterable: new MockIterable() })
return expect(html).to.equal('ab')
})
it('should loop over iterable objects with an offset', function () {
const src = '{% for i in someIterable offset:1 %}{{i}}{%endfor%}'
const html = liquid.parseAndRenderSync(src, { someIterable: new MockIterable() })
return expect(html).to.equal('bc')
})
it('should loop over iterable objects in reverse', function () {
const src = '{% for i in someIterable reversed %}{{i}}{%endfor%}'
const html = liquid.parseAndRenderSync(src, { someIterable: new MockIterable() })
return expect(html).to.equal('cba')
})
it('should go to else for an empty iterable', function () {
const src = '{% for i in emptyIterable reversed %}{{i}}{%else%}EMPTY{%endfor%}'
const html = liquid.parseAndRenderSync(src, { emptyIterable: new MockEmptyIterable() })
return expect(html).to.equal('EMPTY')
})
it('should support iterable names', function () {
const src = '{% for i in someDrop %}{{forloop.name}} {%else%}EMPTY{%endfor%}'
const html = liquid.parseAndRenderSync(src, { someDrop: new MockIterableDrop() })
return expect(html).to.equal('i-someDrop i-someDrop i-someDrop ')
})
})
})
+6
View File
@@ -142,4 +142,10 @@ describe('tags/if', function () {
const html = liquid.parseAndRenderSync(src, scope)
return expect(html).to.equal('true')
})
it('should support async variables', async () => {
const src = `{%if var == 'var' %}success{%endif%}`
const scope = { 'var': Promise.resolve('var') }
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('success')
})
})
@@ -0,0 +1,95 @@
import { Liquid } from '../../../../src/liquid'
import { expect, use } from 'chai'
import * as chaiAsPromised from 'chai-as-promised'
use(chaiAsPromised)
describe('tags/inline-comment', function () {
const liquid = new Liquid()
it('should ignore plain string', async function () {
const src = 'My name is {% # super %} Shopify.'
const html = await liquid.parseAndRender(src)
return expect(html).to.equal('My name is Shopify.')
})
it('should ignore output tokens', async function () {
const src = '{% #\n{{ foo}} \n %}'
const html = await liquid.parseAndRender(src)
return expect(html).to.equal('')
})
it('should support whitespace control', async function () {
const src = '{%- # some comment \n -%}\nfoo'
const html = await liquid.parseAndRender(src)
return expect(html).to.equal('foo')
})
it('should handle hash without trailing whitespace', async function () {
const src = '{% #some comment %}'
const html = await liquid.parseAndRender(src)
return expect(html).to.equal('')
})
it('should handle hash without leading whitespace', async function () {
const src = '{%#some comment %}'
const html = await liquid.parseAndRender(src)
return expect(html).to.equal('')
})
it('should handle empty comment', async function () {
const src = '{%#%}'
const html = await liquid.parseAndRender(src)
return expect(html).to.equal('')
})
it('should support multiple lines', async function () {
const src = [
'{%-',
' # spread inline comments',
' # over multiple lines',
'-%}'
].join('\n')
const html = await liquid.parseAndRender(src)
return expect(html).to.equal('')
})
it('should enforce leading hashes', async function () {
const src = [
'{%-',
' # spread inline comments',
' over multiple lines',
'-%}'
].join('\n')
return expect(liquid.parseAndRender(src))
.to.be.rejectedWith(/every line of an inline comment must start with a '#' character/)
})
describe('sync support', function () {
it('should ignore plain string', function () {
const src = 'My name is {% # super %} Shopify.'
const html = liquid.parseAndRenderSync(src)
return expect(html).to.equal('My name is Shopify.')
})
})
describe('liquid tag', function () {
it('should treat lines starting with a hash as a comment', async function () {
const src = [
'{% liquid ',
' # first comment line',
' # second comment line',
'',
' # another comment line',
' echo \'Hello \'',
'',
' # more comments',
' echo \'goodbye\'',
'-%}'
].join('\n')
const html = await liquid.parseAndRender(src)
return expect(html).to.equal('Hello goodbye')
})
it('should handle lots of hashes', async function () {
const src = [
'{% liquid',
' ##########################',
' # spread inline comments #',
' ##########################',
'-%}'
].join('\n')
const html = await liquid.parseAndRender(src)
return expect(html).to.equal('')
})
})
})
+14
View File
@@ -155,6 +155,20 @@ describe('tags/render', function () {
const html = await liquid.renderFile('index.html', { colors: ['red', 'green'] })
expect(html).to.equal('1: red\n2: green\n')
})
it('should support for <iterable> as', async function () {
class MockIterable {
* [Symbol.iterator] () {
yield 'red'
yield 'green'
}
}
mock({
'/index.html': '{% render "item" for colors as color %}',
'/item.html': '{{forloop.index}}: {{color}}\n'
})
const html = await liquid.renderFile('index.html', { colors: new MockIterable() })
expect(html).to.equal('1: red\n2: green\n')
})
it('should support for <non-array> as', async function () {
mock({
'/index.html': '{% render "item" for "green" as color %}',
+17
View File
@@ -24,6 +24,23 @@ describe('tags/tablerow', function () {
return expect(html).to.equal(dst)
})
it('should support iterables', async function () {
class MockIterable {
* [Symbol.iterator] () {
yield 1
yield 2
yield 3
}
}
const src = '{% tablerow i in someIterable %}{{ i }}{% endtablerow %}'
const ctx = {
someIterable: new MockIterable()
}
const dst = '<tr class="row1"><td class="col1">1</td><td class="col2">2</td><td class="col3">3</td></tr>'
const html = await liquid.parseAndRender(src, ctx)
return expect(html).to.equal(dst)
})
it('should support cols', async function () {
const src = '{% tablerow i in alpha cols:2 %}{{ i }}{% endtablerow %}'
const ctx = {
+11
View File
@@ -61,4 +61,15 @@ describe('drop/drop', function () {
const html = await liquid.parseAndRender(`{{obj.foo}}`, { obj: new PromiseDrop() })
expect(html).to.equal('FOO')
})
it('should respect valueOf', async () => {
class CustomDrop extends Drop {
prop = 'not enumerable'
valueOf () {
return ['foo', 'bar']
}
}
const tpl = '{{drop}}: {% for field in drop %}{{ field }};{% endfor %}'
const html = await liquid.parseAndRender(tpl, { drop: new CustomDrop() })
expect(html).to.equal('foobar: foo;bar;')
})
})
+5 -3
View File
@@ -126,11 +126,13 @@ describe('LiquidOptions#cache', function () {
extname: '.html',
cache: true
})
try { await engine.renderFile('foo') } catch (err) {}
try {
await engine.renderFile('foo')
} catch (err) {}
mock({ '/root/foo.html': 'foo' })
const y = await engine.renderFile('foo')
expect(y).to.equal('foo')
const html = await engine.renderFile('foo')
expect(html).to.equal('foo')
})
})
+32
View File
@@ -52,6 +52,38 @@ describe('Liquid', function () {
const src = '{{ foo }}'
return expect(engine.parseAndRender(src, {}, { strictVariables: true })).rejectedWith(/undefined variable/)
})
it('should support async variables in output', async () => {
const src = '{{ foo }}'
const html = await engine.parseAndRender(src, { foo: Promise.resolve('FOO') })
expect(html).to.equal('FOO')
})
})
describe('#parseAndRenderSync', function () {
const engine = new Liquid()
it('should parse and render variable output', function () {
const html = engine.parseAndRenderSync('{{"foo"}}')
expect(html).to.equal('foo')
})
it('should parse and render complex output', function () {
const tpl = '{{ "Welcome|to]Liquid" | split: "|" | join: "("}}'
const html = engine.parseAndRenderSync(tpl)
expect(html).to.equal('Welcome(to]Liquid')
})
it('should support for-in with variable', function () {
const src = '{% assign total = 3 | minus: 1 %}' +
'{% for i in (1..total) %}{{ i }}{% endfor %}'
const html = engine.parseAndRenderSync(src, {})
return expect(html).to.equal('12')
})
it('should support `globals` render option', function () {
const src = '{{ foo }}'
const html = engine.parseAndRenderSync(src, {}, { globals: { foo: 'FOO' } })
return expect(html).to.equal('FOO')
})
it('should support `strictVariables` render option', function () {
const src = '{{ foo }}'
return expect(() => engine.parseAndRenderSync(src, {}, { strictVariables: true })).throw(/undefined variable/)
})
})
describe('#express()', function () {
const liquid = new Liquid({ root: '/root' })
+42
View File
@@ -0,0 +1,42 @@
import { expect } from 'chai'
import { Liquid } from '../../../src/liquid'
describe('LiquidOptions#*outputEscape*', function () {
it('when outputEscape is not set', async function () {
const engine = new Liquid()
const html = await engine.parseAndRender('{{"<"}}')
expect(html).to.equal('<')
})
it('should escape when outputEscape="escape"', async function () {
const engine = new Liquid({
outputEscape: 'escape'
})
const html = await engine.parseAndRender('{{"<"}}')
expect(html).to.equal('&lt;')
})
it('should json stringify when outputEscape="json"', async function () {
const engine = new Liquid({
outputEscape: 'json'
})
const html = await engine.parseAndRender('{{"<"}}')
expect(html).to.equal('"<"')
})
it('should support outputEscape=Function', async function () {
const engine = new Liquid({
outputEscape: (v: any) => `{${v}}`
})
const html = await engine.parseAndRender('{{"<"}}')
expect(html).to.equal('{<}')
})
it('should skip escape for output with filter "| raw"', async function () {
const engine = new Liquid({
outputEscape: 'escape'
})
const html = await engine.parseAndRender('{{"<" | raw}}')
expect(html).to.equal('<')
})
})
+8 -1
View File
@@ -22,7 +22,8 @@ describe('Context', function () {
zoo: 'coo',
'Mr.Smith': 'John',
arr: ['a', 'b']
}
},
arr: ['a', 'b', 'c', 'd']
}
ctx = new Context(scope)
})
@@ -69,6 +70,12 @@ describe('Context', function () {
it('should read .last of array', async function () {
expect(ctx.get(['bar', 'arr', 'last'])).to.equal('b')
})
it('should read element of array', async function () {
expect(ctx.get(['arr', 1])).to.equal('b')
})
it('should read element of array from end', async function () {
expect(ctx.get(['arr', -2])).to.equal('c')
})
it('should call function', async function () {
expect(ctx.get(['func'])).to.equal('FUNC')
})
-10
View File
@@ -1,10 +0,0 @@
import { expect } from 'chai'
import { Drop } from '../../../src/drop/drop'
describe('drop/drop', function () {
class CustomDrop extends Drop { }
it('.valueOf() should return undefined by default', async function () {
expect(new CustomDrop().valueOf()).to.be.undefined
})
})
+29
View File
@@ -525,4 +525,33 @@ describe('Tokenizer', function () {
expect(() => tokenizer.readLiquidTagTokens()).to.throw(/illegal liquid tag syntax/)
})
})
describe('#read inline comment tags', () => {
it('should allow hash characters in tag names', () => {
const tokenizer = new Tokenizer('{% # some comment %}', trie)
const tokens = tokenizer.readTopLevelTokens()
expect(tokens.length).to.equal(1)
const tag = tokens[0] as TagToken
expect(tag).instanceOf(TagToken)
expect(tag.name).to.equal('#')
expect(tag.args).to.equal('some comment')
})
it('should handle leading whitespace', () => {
const tokenizer = new Tokenizer('{%\n # some comment %}', trie)
const tokens = tokenizer.readTopLevelTokens()
expect(tokens.length).to.equal(1)
const tag = tokens[0] as TagToken
expect(tag).instanceOf(TagToken)
expect(tag.name).to.equal('#')
expect(tag.args).to.equal('some comment')
})
it('should handle no trailing whitespace', () => {
const tokenizer = new Tokenizer('{%\n #some comment %}', trie)
const tokens = tokenizer.readTopLevelTokens()
expect(tokens.length).to.equal(1)
const tag = tokens[0] as TagToken
expect(tag).instanceOf(TagToken)
expect(tag.name).to.equal('#')
expect(tag.args).to.equal('some comment')
})
})
})
+67 -42
View File
@@ -1,7 +1,8 @@
import { Tokenizer } from '../../../src/parser/tokenizer'
import { expect } from 'chai'
import { Drop } from '../../../src/drop/drop'
import { Context } from '../../../src/context/context'
import { toThenable } from '../../../src/util/async'
import { toPromise, toValueSync } from '../../../src/util/async'
import { defaultOperators } from '../../../src/render/operator'
import { createTrie } from '../../../src/util/operator-trie'
@@ -11,7 +12,7 @@ describe('Expression', function () {
const create = (str: string) => new Tokenizer(str, trie).readExpression()
it('should throw when context not defined', done => {
toThenable(create('foo').evaluate(undefined!, false))
toPromise(create('foo').evaluate(undefined!, false))
.then(() => done(new Error('should not resolved')))
.catch(err => {
expect(err.message).to.match(/context not defined/)
@@ -21,19 +22,19 @@ describe('Expression', function () {
describe('single value', function () {
it('should eval literal', async function () {
expect(await toThenable(create('2.4').evaluate(ctx, false))).to.equal(2.4)
expect(await toThenable(create('"foo"').evaluate(ctx, false))).to.equal('foo')
expect(await toThenable(create('false').evaluate(ctx, false))).to.equal(false)
expect(await toPromise(create('2.4').evaluate(ctx, false))).to.equal(2.4)
expect(await toPromise(create('"foo"').evaluate(ctx, false))).to.equal('foo')
expect(await toPromise(create('false').evaluate(ctx, false))).to.equal(false)
})
it('should eval range expression', async function () {
const ctx = new Context({ two: 2 })
expect(await toThenable(create('(2..4)').evaluate(ctx, false))).to.deep.equal([2, 3, 4])
expect(await toThenable(create('(two..4)').evaluate(ctx, false))).to.deep.equal([2, 3, 4])
expect(await toPromise(create('(2..4)').evaluate(ctx, false))).to.deep.equal([2, 3, 4])
expect(await toPromise(create('(two..4)').evaluate(ctx, false))).to.deep.equal([2, 3, 4])
})
it('should eval literal', async function () {
expect(await toThenable(create('2.4').evaluate(ctx, false))).to.equal(2.4)
expect(await toThenable(create('"foo"').evaluate(ctx, false))).to.equal('foo')
expect(await toThenable(create('false').evaluate(ctx, false))).to.equal(false)
expect(await toPromise(create('2.4').evaluate(ctx, false))).to.equal(2.4)
expect(await toPromise(create('"foo"').evaluate(ctx, false))).to.equal('foo')
expect(await toPromise(create('false').evaluate(ctx, false))).to.equal(false)
})
it('should eval property access', async function () {
@@ -42,112 +43,136 @@ describe('Expression', function () {
coo: 'bar',
doo: { foo: 'bar', bar: { foo: 'bar' } }
})
expect(await toThenable(create('foo.bar').evaluate(ctx, false))).to.equal('BAR')
expect(await toThenable(create('foo["bar"]').evaluate(ctx, false))).to.equal('BAR')
expect(await toThenable(create('foo[coo]').evaluate(ctx, false))).to.equal('BAR')
expect(await toThenable(create('foo[doo.foo]').evaluate(ctx, false))).to.equal('BAR')
expect(await toThenable(create('foo[doo["foo"]]').evaluate(ctx, false))).to.equal('BAR')
expect(await toThenable(create('doo[coo].foo').evaluate(ctx, false))).to.equal('bar')
expect(await toPromise(create('foo.bar').evaluate(ctx, false))).to.equal('BAR')
expect(await toPromise(create('foo["bar"]').evaluate(ctx, false))).to.equal('BAR')
expect(await toPromise(create('foo[coo]').evaluate(ctx, false))).to.equal('BAR')
expect(await toPromise(create('foo[doo.foo]').evaluate(ctx, false))).to.equal('BAR')
expect(await toPromise(create('foo[doo["foo"]]').evaluate(ctx, false))).to.equal('BAR')
expect(await toPromise(create('doo[coo].foo').evaluate(ctx, false))).to.equal('bar')
})
})
describe('simple expression', function () {
it('should return false for "1==2"', async () => {
expect(await toThenable(create('1==2').evaluate(ctx, false))).to.equal(false)
expect(await toPromise(create('1==2').evaluate(ctx, false))).to.equal(false)
})
it('should return true for "1<2"', async () => {
expect(await toThenable(create('1<2').evaluate(ctx, false))).to.equal(true)
expect(await toPromise(create('1<2').evaluate(ctx, false))).to.equal(true)
})
it('should return true for "1 < 2"', async () => {
expect(await toThenable(create('1 < 2').evaluate(ctx, false))).to.equal(true)
expect(await toPromise(create('1 < 2').evaluate(ctx, false))).to.equal(true)
})
it('should return true for "1 < 2"', async () => {
expect(await toThenable(create('1 < 2').evaluate(ctx, false))).to.equal(true)
expect(await toPromise(create('1 < 2').evaluate(ctx, false))).to.equal(true)
})
it('should return true for "2 <= 2"', async () => {
expect(await toThenable(create('2 <= 2').evaluate(ctx, false))).to.equal(true)
expect(await toPromise(create('2 <= 2').evaluate(ctx, false))).to.equal(true)
})
it('should return true for "one <= two"', async () => {
const ctx = new Context({ one: 1, two: 2 })
expect(await toThenable(create('one <= two').evaluate(ctx, false))).to.equal(true)
expect(await toPromise(create('one <= two').evaluate(ctx, false))).to.equal(true)
})
it('should return false for "x contains "x""', async () => {
const ctx = new Context({ x: 'XXX' })
expect(await toThenable(create('x contains "x"').evaluate(ctx, false))).to.equal(false)
expect(await toPromise(create('x contains "x"').evaluate(ctx, false))).to.equal(false)
})
it('should return true for "x contains "X""', async () => {
const ctx = new Context({ x: 'XXX' })
expect(await toThenable(create('x contains "X"').evaluate(ctx, false))).to.equal(true)
expect(await toPromise(create('x contains "X"').evaluate(ctx, false))).to.equal(true)
})
it('should return false for "1 contains "x""', async () => {
const ctx = new Context({ x: 'XXX' })
expect(await toThenable(create('1 contains "x"').evaluate(ctx, false))).to.equal(false)
expect(await toPromise(create('1 contains "x"').evaluate(ctx, false))).to.equal(false)
})
it('should return false for "y contains "x""', async () => {
const ctx = new Context({ x: 'XXX' })
expect(await toThenable(create('y contains "x"').evaluate(ctx, false))).to.equal(false)
expect(await toPromise(create('y contains "x"').evaluate(ctx, false))).to.equal(false)
})
it('should return false for "z contains "x""', async () => {
const ctx = new Context({ x: 'XXX' })
expect(await toThenable(create('z contains "x"').evaluate(ctx, false))).to.equal(false)
expect(await toPromise(create('z contains "x"').evaluate(ctx, false))).to.equal(false)
})
it('should return true for "(1..5) contains 3"', async () => {
const ctx = new Context({ x: 'XXX' })
expect(await toThenable(create('(1..5) contains 3').evaluate(ctx, false))).to.equal(true)
expect(await toPromise(create('(1..5) contains 3').evaluate(ctx, false))).to.equal(true)
})
it('should return false for "(1..5) contains 6"', async () => {
const ctx = new Context({ x: 'XXX' })
expect(await toThenable(create('(1..5) contains 6').evaluate(ctx, false))).to.equal(false)
expect(await toPromise(create('(1..5) contains 6').evaluate(ctx, false))).to.equal(false)
})
it('should return true for ""<=" == "<=""', async () => {
expect(await toThenable(create('"<=" == "<="').evaluate(ctx, false))).to.equal(true)
expect(await toPromise(create('"<=" == "<="').evaluate(ctx, false))).to.equal(true)
})
})
it('should allow space in quoted value', async function () {
const ctx = new Context({ space: ' ' })
expect(await toThenable(create('" " == space').evaluate(ctx, false))).to.equal(true)
expect(await toPromise(create('" " == space').evaluate(ctx, false))).to.equal(true)
})
describe('escape', () => {
it('should escape quote', async function () {
const ctx = new Context({ quote: '"' })
expect(await toThenable(create('"\\"" == quote').evaluate(ctx, false))).to.equal(true)
expect(await toPromise(create('"\\"" == quote').evaluate(ctx, false))).to.equal(true)
})
it('should escape square bracket', async function () {
const ctx = new Context({ obj: { ']': 'bracket' } })
expect(await toThenable(create('obj["]"] == "bracket"').evaluate(ctx, false))).to.equal(true)
expect(await toPromise(create('obj["]"] == "bracket"').evaluate(ctx, false))).to.equal(true)
})
})
describe('complex expression', function () {
it('should support value or value', async function () {
expect(await toThenable(create('false or true').evaluate(ctx, false))).to.equal(true)
expect(await toPromise(create('false or true').evaluate(ctx, false))).to.equal(true)
})
it('should support < and contains', async function () {
expect(await toThenable(create('1 < 2 and x contains "x"').evaluate(ctx, false))).to.equal(false)
expect(await toPromise(create('1 < 2 and x contains "x"').evaluate(ctx, false))).to.equal(false)
})
it('should support < or contains', async function () {
expect(await toThenable(create('1 < 2 or x contains "x"').evaluate(ctx, false))).to.equal(true)
expect(await toPromise(create('1 < 2 or x contains "x"').evaluate(ctx, false))).to.equal(true)
})
it('should support Drops for "x contains "x""', async () => {
class TemplateDrop extends Drop {
valueOf () { return 'X' }
}
const ctx = new Context({ x: 'XXX', X: new TemplateDrop() })
expect(await toPromise(create('x contains X').evaluate(ctx, false))).to.equal(true)
})
it('should support value and !=', async function () {
const ctx = new Context({ empty: '' })
expect(await toThenable(create('empty and empty != ""').evaluate(ctx, false))).to.equal(false)
expect(await toPromise(create('empty and empty != ""').evaluate(ctx, false))).to.equal(false)
})
it('should recognize quoted value', async function () {
expect(await toThenable(create('">"').evaluate(ctx, false))).to.equal('>')
expect(await toPromise(create('">"').evaluate(ctx, false))).to.equal('>')
})
it('should evaluate from right to left', async function () {
expect(await toThenable(create('true or false and false').evaluate(ctx, false))).to.equal(true)
expect(await toThenable(create('true and false and false or true').evaluate(ctx, false))).to.equal(false)
expect(await toPromise(create('true or false and false').evaluate(ctx, false))).to.equal(true)
expect(await toPromise(create('true and false and false or true').evaluate(ctx, false))).to.equal(false)
})
it('should recognize property access', async function () {
const ctx = new Context({ obj: { foo: true } })
expect(await toThenable(create('obj["foo"] and true').evaluate(ctx, false))).to.equal(true)
expect(await toPromise(create('obj["foo"] and true').evaluate(ctx, false))).to.equal(true)
})
it('should allow nested property access', async function () {
const ctx = new Context({ obj: { foo: 'FOO' }, keys: { "what's this": 'foo' } })
expect(await toThenable(create('obj[keys["what\'s this"]]').evaluate(ctx, false))).to.equal('FOO')
expect(await toPromise(create('obj[keys["what\'s this"]]').evaluate(ctx, false))).to.equal('FOO')
})
})
describe('sync', function () {
it('should eval literal', function () {
expect(toValueSync(create('2.4').evaluate(ctx, false))).to.equal(2.4)
})
it('should return false for "1==2"', () => {
expect(toValueSync(create('1==2').evaluate(ctx, false))).to.equal(false)
})
it('should escape quote', function () {
const ctx = new Context({ quote: '"' })
expect(toValueSync(create('"\\"" == quote').evaluate(ctx, false))).to.equal(true)
})
it('should allow nested property access', function () {
const ctx = new Context({ obj: { foo: 'FOO' }, keys: { "what's this": 'foo' } })
expect(toValueSync(create('obj[keys["what\'s this"]]').evaluate(ctx, false))).to.equal('FOO')
})
})
})
+2 -2
View File
@@ -4,7 +4,7 @@ import { HTMLToken } from '../../../src/tokens/html-token'
import { Render } from '../../../src/render/render'
import { HTML } from '../../../src/template/html'
import { SimpleEmitter } from '../../../src/emitters/simple-emitter'
import { toThenable } from '../../../src/util/async'
import { toPromise } from '../../../src/util/async'
import { Tag } from '../../../src/template/tag/tag'
import { TagToken } from '../../../src/types'
@@ -18,7 +18,7 @@ describe('render', function () {
it('should render html', async function () {
const scope = new Context()
const token = { getContent: () => '<p>' } as HTMLToken
const html = await toThenable(render.renderTemplates([new HTML(token)], scope, new SimpleEmitter()))
const html = await toPromise(render.renderTemplates([new HTML(token)], scope, new SimpleEmitter()))
return expect(html).to.equal('<p>')
})
})
+9 -9
View File
@@ -2,7 +2,7 @@ import * as chai from 'chai'
import * as sinon from 'sinon'
import * as sinonChai from 'sinon-chai'
import { Context } from '../../../../src/context/context'
import { toThenable } from '../../../../src/util/async'
import { toPromise } from '../../../../src/util/async'
import { NumberToken } from '../../../../src/tokens/number-token'
import { QuotedToken } from '../../../../src/tokens/quoted-token'
import { IdentifierToken } from '../../../../src/tokens/identifier-token'
@@ -25,46 +25,46 @@ describe('filter', function () {
})
it('should render input if filter not registered', async function () {
expect(await toThenable(filters.create('undefined', []).render('foo', ctx))).to.equal('foo')
expect(await toPromise(filters.create('undefined', []).render('foo', ctx))).to.equal('foo')
})
it('should call filter impl with correct arguments', async function () {
const spy = sinon.spy()
filters.set('foo', spy)
const thirty = new NumberToken(new IdentifierToken('30', 0, 2), undefined)
await toThenable(filters.create('foo', [thirty]).render('foo', ctx))
await toPromise(filters.create('foo', [thirty]).render('foo', ctx))
expect(spy).to.have.been.calledWith('foo', 30)
})
it('should call filter impl with correct this', async function () {
const spy = sinon.spy()
filters.set('foo', spy)
const thirty = new NumberToken(new IdentifierToken('33', 0, 2), undefined)
await toThenable(filters.create('foo', [thirty]).render('foo', ctx))
await toPromise(filters.create('foo', [thirty]).render('foo', ctx))
expect(spy).to.have.been.calledOn(sinon.match.has('context', ctx))
expect(spy).to.have.been.calledOn(sinon.match.has('liquid', liquid))
})
it('should render a simple filter', async function () {
filters.set('upcase', x => x.toUpperCase())
expect(await toThenable(filters.create('upcase', []).render('foo', ctx))).to.equal('FOO')
expect(await toPromise(filters.create('upcase', []).render('foo', ctx))).to.equal('FOO')
})
it('should render filters with argument', async function () {
filters.set('add', (a, b) => a + b)
const two = new NumberToken(new IdentifierToken('2', 0, 1), undefined)
expect(await toThenable(filters.create('add', [two]).render(3, ctx))).to.equal(5)
expect(await toPromise(filters.create('add', [two]).render(3, ctx))).to.equal(5)
})
it('should render filters with multiple arguments', async function () {
filters.set('add', (a, b, c) => a + b + c)
const two = new NumberToken(new IdentifierToken('2', 0, 1), undefined)
const c = new QuotedToken('"c"', 0, 3)
expect(await toThenable(filters.create('add', [two, c]).render(3, ctx))).to.equal('5c')
expect(await toPromise(filters.create('add', [two, c]).render(3, ctx))).to.equal('5c')
})
it('should pass Objects/Drops as it is', async function () {
filters.set('name', a => a.constructor.name)
class Foo {}
expect(await toThenable(filters.create('name', []).render(new Foo(), ctx))).to.equal('Foo')
expect(await toPromise(filters.create('name', []).render(new Foo(), ctx))).to.equal('Foo')
})
it('should not throw when filter name illegal', function () {
@@ -76,6 +76,6 @@ describe('filter', function () {
it('should support key value pairs', async function () {
filters.set('add', (a, b) => b[0] + ':' + (a + b[1]))
const two = new NumberToken(new IdentifierToken('2', 0, 1), undefined)
expect(await toThenable((filters.create('add', [['num', two]]).render(3, ctx)))).to.equal('num:5')
expect(await toPromise(filters.create('add', [['num', two]]).render(3, ctx))).to.equal('num:5')
})
})
+8 -8
View File
@@ -1,5 +1,5 @@
import * as chai from 'chai'
import { toThenable } from '../../../src/util/async'
import { toPromise } from '../../../src/util/async'
import { Hash } from '../../../src/template/tag/hash'
import { Context } from '../../../src/context/context'
@@ -7,34 +7,34 @@ const expect = chai.expect
describe('Hash', function () {
it('should parse "reverse"', async function () {
const hash = await toThenable(new Hash('reverse').render(new Context({ foo: 3 })))
const hash = await toPromise(new Hash('reverse').render(new Context({ foo: 3 })))
expect(hash).to.haveOwnProperty('reverse')
expect(hash.reverse).to.be.true
})
it('should parse "num:foo"', async function () {
const hash = await toThenable(new Hash('num:foo').render(new Context({ foo: 3 })))
const hash = await toPromise(new Hash('num:foo').render(new Context({ foo: 3 })))
expect(hash.num).to.equal(3)
})
it('should parse "num:3"', async function () {
const hash = await toThenable(new Hash('num:3').render(new Context()))
const hash = await toPromise(new Hash('num:3').render(new Context()))
expect(hash.num).to.equal(3)
})
it('should parse "num: arr[0]"', async function () {
const hash = await toThenable(new Hash('num:3').render(new Context({ arr: [3] })))
const hash = await toPromise(new Hash('num:3').render(new Context({ arr: [3] })))
expect(hash.num).to.equal(3)
})
it('should parse "num: 2.3"', async function () {
const hash = await toThenable(new Hash('num:2.3').render(new Context()))
const hash = await toPromise(new Hash('num:2.3').render(new Context()))
expect(hash.num).to.equal(2.3)
})
it('should parse "num:bar.coo"', async function () {
const pending = new Hash('num:bar.coo').render(new Context({ bar: { coo: 3 } }))
const hash = await toThenable(pending)
const hash = await toPromise(pending)
expect(hash.num).to.equal(3)
})
it('should parse "num1:2.3 reverse,num2:bar.coo\n num3: arr[0]"', async function () {
const ctx = new Context({ bar: { coo: 3 }, arr: [4] })
const hash = await toThenable(new Hash('num1:2.3 reverse,num2:bar.coo\n num3: arr[0]').render(ctx))
const hash = await toPromise(new Hash('num1:2.3 reverse,num2:bar.coo\n num3: arr[0]').render(ctx))
expect(hash).to.deep.equal({
num1: 2.3,
reverse: true,
+9 -9
View File
@@ -1,5 +1,5 @@
import * as chai from 'chai'
import { toThenable } from '../../../src/util/async'
import { toPromise } from '../../../src/util/async'
import { Context } from '../../../src/context/context'
import { Output } from '../../../src/template/output'
import { OutputToken } from '../../../src/tokens/output-token'
@@ -21,25 +21,25 @@ describe('Output', function () {
foo: { obj: { arr: ['a', 2] } }
})
const output = new Output({ content: 'foo' } as OutputToken, liquid)
await toThenable(output.render(scope, emitter))
await toPromise(output.render(scope, emitter))
return expect(emitter.html).to.equal('[object Object]')
})
it('should skip function property', async function () {
const scope = new Context({ obj: { foo: 'foo', bar: (x: any) => x } })
const output = new Output({ content: 'obj' } as OutputToken, liquid)
await toThenable(output.render(scope, emitter))
await toPromise(output.render(scope, emitter))
return expect(emitter.html).to.equal('[object Object]')
})
it('should respect to .toString()', async () => {
const scope = new Context({ obj: { toString: () => 'FOO' } })
const output = new Output({ content: 'obj' } as OutputToken, liquid)
await toThenable(output.render(scope, emitter))
await toPromise(output.render(scope, emitter))
return expect(emitter.html).to.equal('FOO')
})
it('should respect to .toString()', async () => {
const scope = new Context({ obj: { toString: () => 'FOO' } })
const output = new Output({ content: 'obj' } as OutputToken, liquid)
await toThenable(output.render(scope, emitter))
await toPromise(output.render(scope, emitter))
return expect(emitter.html).to.equal('FOO')
})
context('when keepOutputType is enabled', () => {
@@ -62,7 +62,7 @@ describe('Output', function () {
foo: 42
}, { ...defaultOptions, keepOutputType: true })
const output = new Output({ content: 'foo' } as OutputToken, liquid)
await toThenable(output.render(scope, emitter))
await toPromise(output.render(scope, emitter))
return expect(emitter.html).to.equal(42)
})
it('should respect output variable boolean type', async () => {
@@ -70,7 +70,7 @@ describe('Output', function () {
foo: true
}, { ...defaultOptions, keepOutputType: true })
const output = new Output({ content: 'foo' } as OutputToken, liquid)
await toThenable(output.render(scope, emitter))
await toPromise(output.render(scope, emitter))
return expect(emitter.html).to.equal(true)
})
it('should respect output variable object type', async () => {
@@ -78,7 +78,7 @@ describe('Output', function () {
foo: 'test'
}, { ...defaultOptions, keepOutputType: true })
const output = new Output({ content: 'foo' } as OutputToken, liquid)
await toThenable(output.render(scope, emitter))
await toPromise(output.render(scope, emitter))
return expect(emitter.html).to.equal('test')
})
it('should respect output variable string type', async () => {
@@ -86,7 +86,7 @@ describe('Output', function () {
foo: { a: { b: 42 } }
}, { ...defaultOptions, keepOutputType: true })
const output = new Output({ content: 'foo' } as OutputToken, liquid)
await toThenable(output.render(scope, emitter))
await toPromise(output.render(scope, emitter))
return expect(emitter.html).to.deep.equal({ a: { b: 42 } })
})
})
+2 -2
View File
@@ -4,7 +4,7 @@ import { Context } from '../../../src/context/context'
import * as sinon from 'sinon'
import * as sinonChai from 'sinon-chai'
import { TagToken } from '../../../src/tokens/tag-token'
import { toThenable } from '../../../src/util/async'
import { toPromise } from '../../../src/util/async'
chai.use(sinonChai)
const expect = chai.expect
@@ -20,7 +20,7 @@ describe('Tag', function () {
args: '',
name: 'foo'
} as TagToken
await toThenable(new Tag(token, [], {
await toPromise(new Tag(token, [], {
tags: {
get: () => ({ render: spy })
}
+2 -2
View File
@@ -1,7 +1,7 @@
import * as chai from 'chai'
import { Liquid } from '../../../src/liquid'
import { QuotedToken } from '../../../src/tokens/quoted-token'
import { toThenable } from '../../../src/util/async'
import { toPromise } from '../../../src/util/async'
import * as sinonChai from 'sinon-chai'
import * as sinon from 'sinon'
import { Context } from '../../../src/context/context'
@@ -36,7 +36,7 @@ describe('Value', function () {
const scope = new Context({
foo: { bar: 'bar' }
})
await toThenable(tpl.value(scope, false))
await toPromise(tpl.value(scope, false))
expect(date).to.have.been.calledWith('bar', 'b')
expect(time).to.have.been.calledWith('y', 2)
})
+26 -25
View File
@@ -1,4 +1,4 @@
import { toThenable, toPromise, toValue } from '../../../src/util/async'
import { toPromise, toValueSync } from '../../../src/util/async'
import { expect, use } from 'chai'
import * as chaiAsPromised from 'chai-as-promised'
@@ -13,47 +13,45 @@ describe('utils/async', () => {
const result = await toPromise(foo())
expect(result).to.equal('foo')
})
})
describe('#toThenable()', function () {
it('should support iterable with single return statement', async () => {
function * foo () {
return 'foo'
}
const result = await toThenable(foo())
const result = await toPromise(foo())
expect(result).to.equal('foo')
})
it('should support promise', async () => {
function foo () {
return Promise.resolve('foo')
}
const result = await toThenable(foo())
const result = await toPromise(foo())
expect(result).to.equal('foo')
})
it('should resolve dependency', async () => {
function * foo () {
function * foo (): Generator<Generator<string>> {
return yield bar()
}
function * bar () {
function * bar (): Generator<string> {
return 'bar'
}
const result = await toThenable(foo())
const result = await toPromise(foo())
expect(result).to.equal('bar')
})
it('should support promise dependency', async () => {
function * foo () {
function * foo (): Generator<Promise<string>> {
return yield Promise.resolve('foo')
}
const result = await toThenable(foo())
const result = await toPromise(foo())
expect(result).to.equal('foo')
})
it('should reject Promise if dependency throws syncly', done => {
function * foo () {
function * foo (): Generator<Generator<never>> {
return yield bar()
}
function * bar (): IterableIterator<any> {
function * bar (): Generator<never> {
throw new Error('bar')
}
toThenable(foo()).catch(err => {
toPromise(foo()).catch(err => {
expect(err.message).to.equal('bar')
done()
return 0 as any
@@ -70,46 +68,49 @@ describe('utils/async', () => {
ret += 'foo'
return ret
}
function * bar (): IterableIterator<any> {
function * bar (): Generator<never> {
throw new Error('bar')
}
const ret = await toThenable(foo())
const ret = await toPromise(foo())
expect(ret).to.equal('barfoo')
})
})
describe('#toValue()', function () {
describe('#toValueSync()', function () {
it('should throw Error if dependency throws syncly', () => {
function * foo () {
function * foo (): Generator<Generator<never>> {
return yield bar()
}
function * bar (): IterableIterator<any> {
function * bar (): Generator<never> {
throw new Error('bar')
}
expect(() => toValue(foo())).to.throw('bar')
expect(() => toValueSync(foo())).to.throw('bar')
})
it('should resume yield after catch', () => {
function * foo () {
function * foo (): Generator<unknown, never, never> {
try {
yield bar()
} catch (e) {}
return yield 'foo'
}
function * bar (): IterableIterator<any> {
function * bar (): Generator<never> {
throw new Error('bar')
}
expect(toValue(foo())).to.equal('foo')
expect(toValueSync(foo())).to.equal('foo')
})
it('should resume return after catch', () => {
function * foo () {
function * foo (): Generator<Generator<never>, string> {
try {
yield bar()
} catch (e) {}
return 'foo'
}
function * bar (): IterableIterator<any> {
function * bar (): Generator<never> {
throw new Error('bar')
}
expect(toValue(foo())).to.equal('foo')
expect(toValueSync(foo())).to.equal('foo')
})
it('should return non iterator value as it is', () => {
expect(toValueSync('foo')).to.equal('foo')
})
})
})