Compare commits

...
72 Commits
Author SHA1 Message Date
semantic-release-bot 626f911503 chore(release): 9.19.0 [skip ci]
# [9.19.0](https://github.com/harttle/liquidjs/compare/v9.18.0...v9.19.0) (2020-12-18)

### Bug Fixes

* lint ([de32259](https://github.com/harttle/liquidjs/commit/de32259))
* move offset adding complexity inside TimezoneDate ([26b2175](https://github.com/harttle/liquidjs/commit/26b2175))
* simpler timezone regex and non-null offset ([e3ecfe3](https://github.com/harttle/liquidjs/commit/e3ecfe3))

### Features

* add preserveTimezones option ([d70cd2a](https://github.com/harttle/liquidjs/commit/d70cd2a))
* parse and handle date timezone offsets ([c16c787](https://github.com/harttle/liquidjs/commit/c16c787))
2020-12-18 14:07:14 +00:00
JoonasandJun Yang de32259bb9 fix: lint 2020-12-18 22:03:58 +08:00
JoonasandJun Yang d70cd2a1ea feat: add preserveTimezones option 2020-12-18 22:03:58 +08:00
JoonasandJun Yang 26b217542b fix: move offset adding complexity inside TimezoneDate 2020-12-18 22:03:58 +08:00
JoonasandJun Yang e3ecfe3adc fix: simpler timezone regex and non-null offset 2020-12-18 22:03:58 +08:00
wyoziandJun Yang c16c787861 feat: parse and handle date timezone offsets 2020-12-18 22:03:58 +08:00
semantic-release-bot 13f5f95d01 chore(release): 9.18.0 [skip ci]
# [9.18.0](https://github.com/harttle/liquidjs/compare/v9.17.0...v9.18.0) (2020-12-17)

### Bug Fixes

* address refactor comments ([6a0ad10](https://github.com/harttle/liquidjs/commit/6a0ad10))

### Features

* add option for keeping variable type in output ([cd92e77](https://github.com/harttle/liquidjs/commit/cd92e77))
2020-12-17 15:17:09 +00:00
harttle c8db5e6259 docs: try fix special chars in changelog.md 2020-12-17 23:08:54 +08:00
allcontributors[bot]andJun Yang 9e511ecd14 docs: update .all-contributorsrc [skip ci] 2020-12-16 10:23:26 +08:00
allcontributors[bot]andJun Yang 45d95bb8d8 docs: update README.md [skip ci] 2020-12-16 10:23:26 +08:00
PixcellandJun Yang 8d3eca6ee2 stringify value in emitter when concatenating it 2020-12-16 10:20:10 +08:00
Alex de WergifosseandJun Yang 6a0ad102a8 fix: address refactor comments 2020-12-16 10:20:10 +08:00
Alex de WergifosseandJun Yang b8e4b3337b test: add tests for keepOutputType option 2020-12-16 10:20:10 +08:00
Alex de WergifosseandJun Yang cd92e77726 feat: add option for keeping variable type in output 2020-12-16 10:20:10 +08:00
allcontributors[bot]andJun Yang e84fbf32ad docs: update .all-contributorsrc [skip ci] 2020-12-14 22:49:50 +08:00
allcontributors[bot]andJun Yang 0266b27838 docs: update README.md [skip ci] 2020-12-14 22:49:50 +08:00
semantic-release-bot 090c88eb92 chore(release): 9.17.0 [skip ci]
# [9.17.0](https://github.com/harttle/liquidjs/compare/v9.16.1...v9.17.0) (2020-12-07)

### Bug Fixes

* elsif is not supported for unless, fixes [#268](https://github.com/harttle/liquidjs/issues/268) ([2bbf501](https://github.com/harttle/liquidjs/commit/2bbf501))
* enforce string-type pattern in `replace`, fixes [#243](https://github.com/harttle/liquidjs/issues/243) ([c8afa39](https://github.com/harttle/liquidjs/commit/c8afa39))
* raw block not ignoring {% characters, fixes [#263](https://github.com/harttle/liquidjs/issues/263) ([a492d8e](https://github.com/harttle/liquidjs/commit/a492d8e))

### Features

* passing liquid to FilterImpl, closes [#277](https://github.com/harttle/liquidjs/issues/277) ([f9f595f](https://github.com/harttle/liquidjs/commit/f9f595f))
2020-12-07 16:34:49 +00:00
harttle e15803fae4 test: fix e2e test 2020-12-08 00:30:32 +08:00
harttle 117673a725 style: make eslint check .ts files 2020-12-08 00:24:07 +08:00
harttle ef43120d6e test: fix test cases after rebase 2020-12-08 00:20:43 +08:00
harttle f9f595f0f4 feat: passing liquid to FilterImpl, closes #277 2020-12-08 00:17:27 +08:00
harttle 2bbf50171b fix: elsif is not supported for unless, fixes #268 2020-12-08 00:16:36 +08:00
harttle a492d8e23d fix: raw block not ignoring {% characters, fixes #263 2020-12-08 00:12:20 +08:00
harttle c8afa39a01 fix: enforce string-type pattern in replace, fixes #243 2020-12-08 00:08:31 +08:00
sschuldenzuckerandJun Yang d8f9091a12 add and use error classes for undefined variables
cleans up funky message-matching code in expression.ts for lenient.
2020-12-06 22:58:08 +08:00
sschuldenzuckerandJun Yang ca18a579a1 Expression: minor type change & simplification
pre-fetching postfix simplifies code for 'lenient'
2020-12-06 22:58:08 +08:00
sschuldenzuckerandJun Yang 6f2b24f7ca implement lenientIf for the unless tag, too 2020-12-06 22:58:08 +08:00
sschuldenzuckerandJun Yang 87d7dd0d37 docs for the lenientIf option 2020-12-06 22:58:08 +08:00
sschuldenzuckerandJun Yang 768fb79e32 Implement lenientIf option (resolve #265; default off)
See description of the option in liquid-options.ts.
2020-12-06 22:58:08 +08:00
sschuldenzuckerandJun Yang c920ebb282 fix test: don't pass {} for Context 2020-12-06 22:58:08 +08:00
Jun YangandGitHub 9706e8b411 docs: add a security policy, see #254 2020-11-29 20:59:00 +08:00
dependabot[bot]andJun Yang 75698eefec chore(deps): bump highlight.js from 9.18.1 to 9.18.5 in /docs
Bumps [highlight.js](https://github.com/highlightjs/highlight.js) from 9.18.1 to 9.18.5.
- [Release notes](https://github.com/highlightjs/highlight.js/releases)
- [Changelog](https://github.com/highlightjs/highlight.js/blob/9.18.5/CHANGES.md)
- [Commits](https://github.com/highlightjs/highlight.js/compare/9.18.1...9.18.5)

Signed-off-by: dependabot[bot] <[email protected]>
2020-11-25 21:56:22 +08:00
dependabot[bot]andJun Yang dfd358c0a2 chore(deps): bump highlight.js from 9.15.9 to 9.18.5
Bumps [highlight.js](https://github.com/highlightjs/highlight.js) from 9.15.9 to 9.18.5.
- [Release notes](https://github.com/highlightjs/highlight.js/releases)
- [Changelog](https://github.com/highlightjs/highlight.js/blob/9.18.5/CHANGES.md)
- [Commits](https://github.com/highlightjs/highlight.js/compare/9.15.9...9.18.5)

Signed-off-by: dependabot[bot] <[email protected]>
2020-11-25 21:55:58 +08:00
dependabot[bot]andJun Yang 4c70946fa1 chore(deps-dev): bump semantic-release from 15.13.19 to 17.2.3
Bumps [semantic-release](https://github.com/semantic-release/semantic-release) from 15.13.19 to 17.2.3.
- [Release notes](https://github.com/semantic-release/semantic-release/releases)
- [Commits](https://github.com/semantic-release/semantic-release/compare/v15.13.19...v17.2.3)

Signed-off-by: dependabot[bot] <[email protected]>
2020-11-20 22:11:04 +08:00
Jun YangandGitHub 9d9c0c1ca7 docs: update the UMD url and jsFiddle demo, see #264 2020-11-08 12:18:37 +08:00
semantic-release-bot a9d3a5a402 chore(release): 9.16.1 [skip ci]
## [9.16.1](https://github.com/harttle/liquidjs/compare/v9.16.0...v9.16.1) (2020-10-09)

### Bug Fixes

* braced property access ([18a807e](https://github.com/harttle/liquidjs/commit/18a807e))
2020-10-09 13:22:06 +00:00
wyoziandJun Yang 30dffb675e Test for broken quoted property access 2020-10-09 21:18:40 +08:00
wyoziandJun Yang e67c3c6924 Make end brace peek stricter and fix the test 2020-10-09 21:18:40 +08:00
wyoziandJun Yang e2e410f334 Add incomplete quoted property access test 2020-10-09 21:18:40 +08:00
wyoziandJun Yang 27ce44238e Test getVariableAsText 2020-10-09 21:18:40 +08:00
wyoziandJun Yang abfaf8a042 Test quoted property access tokenizing 2020-10-09 21:18:40 +08:00
wyoziandJun Yang 18a807ea2e fix: braced property access 2020-10-09 21:18:40 +08:00
harttle a6126c3bea docs: add amit777 as contributor for code 2020-10-08 16:57:59 +08:00
harttle a96d772963 docs: format 2020-10-08 14:40:58 +08:00
semantic-release-bot 732ef2605e chore(release): 9.16.0 [skip ci]
# [9.16.0](https://github.com/harttle/liquidjs/compare/v9.15.1...v9.16.0) (2020-10-08)

### Features

* support jsTruthy, [#255](https://github.com/harttle/liquidjs/issues/255) [#257](https://github.com/harttle/liquidjs/issues/257) ([72ee7b4](https://github.com/harttle/liquidjs/commit/72ee7b4))
2020-10-08 05:15:00 +00:00
harttle 72ee7b4c1e feat: support jsTruthy, #255 #257 2020-10-08 13:11:02 +08:00
Amit GuptaandJun Yang f807383f2a resolved issues raised by hhartle 2020-10-08 12:43:37 +08:00
Amit GuptaandJun Yang 02bfed1c57 Added jsTruthy flag to constuctor in order to enable JS style truthiness evaluation 2020-10-08 12:43:37 +08:00
harttle b11df79b91 test: make it pass regardless timezone, #257 2020-10-08 09:38:10 +08:00
semantic-release-bot 8ee9bd7ba8 chore(release): 9.15.1 [skip ci]
## [9.15.1](https://github.com/harttle/liquidjs/compare/v9.15.0...v9.15.1) (2020-10-03)

### Bug Fixes

* allow quoted variable name in capture, fixes [#252](https://github.com/harttle/liquidjs/issues/252) ([5b3f419](https://github.com/harttle/liquidjs/commit/5b3f419))
2020-10-03 14:06:41 +00:00
harttle 5b3f4195f0 fix: allow quoted variable name in capture, fixes #252 2020-10-03 22:03:17 +08:00
dependabot[bot]andJun Yang 236d971705 chore(deps): bump node-fetch from 2.6.0 to 2.6.1
Bumps [node-fetch](https://github.com/bitinn/node-fetch) from 2.6.0 to 2.6.1.
- [Release notes](https://github.com/bitinn/node-fetch/releases)
- [Changelog](https://github.com/node-fetch/node-fetch/blob/master/docs/CHANGELOG.md)
- [Commits](https://github.com/bitinn/node-fetch/compare/v2.6.0...v2.6.1)

Signed-off-by: dependabot[bot] <[email protected]>
2020-10-03 20:36:06 +08:00
Jun YangandGitHub 0cd4d87d46 docs: update the demo on index page, fixes #247 2020-09-11 22:30:21 +08:00
harttle 1fdfd4a68a docs: show social share on small screens 2020-08-12 20:37:07 +08:00
dependabot[bot]andJun Yang c1f73a9f03 chore(deps): bump prismjs from 1.19.0 to 1.21.0 in /docs
Bumps [prismjs](https://github.com/PrismJS/prism) from 1.19.0 to 1.21.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.19.0...v1.21.0)

Signed-off-by: dependabot[bot] <[email protected]>
2020-08-08 22:14:03 +08:00
semantic-release-bot d8f550e447 chore(release): 9.15.0 [skip ci]
# [9.15.0](https://github.com/harttle/liquidjs/compare/v9.14.1...v9.15.0) (2020-08-04)

### Features

* export toPromise and toValue, see [#158](https://github.com/harttle/liquidjs/issues/158) ([2e5ab98](https://github.com/harttle/liquidjs/commit/2e5ab98))
2020-08-04 16:58:57 +00:00
harttle 2e5ab98bfc feat: export toPromise and toValue, see #158 2020-08-05 00:46:01 +08:00
harttle 0dbb7792d7 docs: fix button style 2020-08-01 22:00:12 +08:00
dependabot[bot]andJun Yang e10ce657dd chore(deps): bump elliptic from 6.5.0 to 6.5.3
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.0 to 6.5.3.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.0...v6.5.3)

Signed-off-by: dependabot[bot] <[email protected]>
2020-07-30 22:42:19 +08:00
harttle 3713fa08b2 docs: credit Peter deHaan as contributor 2020-07-25 23:31:48 +08:00
harttle c5002d8cad docs: location.hash for source code 2020-07-25 01:23:47 +08:00
dependabot[bot]andJun Yang 00b0a5708c chore(deps): bump lodash from 4.17.15 to 4.17.19 in /docs
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <[email protected]>
2020-07-20 21:14:42 +08:00
semantic-release-bot f82ee8a18c chore(release): 9.14.1 [skip ci]
## [9.14.1](https://github.com/harttle/liquidjs/compare/v9.14.0...v9.14.1) (2020-07-08)

### Bug Fixes

* enumerate Promises (e.g. in for & tablerow) ([#237](https://github.com/harttle/liquidjs/issues/237)) ([941dd66](https://github.com/harttle/liquidjs/commit/941dd66))
2020-07-08 17:40:43 +00:00
dependabot[bot]andJun Yang 632bb6b077 chore(deps): bump npm from 6.13.4 to 6.14.6
Bumps [npm](https://github.com/npm/cli) from 6.13.4 to 6.14.6.
- [Release notes](https://github.com/npm/cli/releases)
- [Changelog](https://github.com/npm/cli/blob/latest/CHANGELOG.md)
- [Commits](https://github.com/npm/cli/compare/v6.13.4...v6.14.6)

Signed-off-by: dependabot[bot] <[email protected]>
2020-07-09 01:36:21 +08:00
allcontributors[bot]GitHuballcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
0b6ee9bca1 docs: add tejasmanohar as a contributor (#238)
* docs: update README.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-06-28 09:57:45 +08:00
Tejas ManoharandGitHub 941dd668fe fix: enumerate Promises (e.g. in for & tablerow) (#237)
* fix: enumerate Promise<array> (e.g. in {% for ... %})

Previously, a Promise of an array was not being enumerated in
{% for %}, for example. This is misleading since the library
handles promises elsewhere (e.g. if you {% assign x = promiseArray %}
and then {% for v in x %}, it worked just fine.

This PR makes Promises of arrays handled by changing toEnumerable
to handle then-ables. This affects other iterators, too, e.g. tablerow,
so I put in a test for that as well.
2020-06-28 09:55:56 +08:00
semantic-release-bot 11ffd65503 chore(release): 9.14.0 [skip ci]
# [9.14.0](https://github.com/harttle/liquidjs/compare/v9.13.0...v9.14.0) (2020-06-25)

### Features

* setup universal browser and node builds ([6cf6ffa](https://github.com/harttle/liquidjs/commit/6cf6ffa))
2020-06-25 12:14:45 +00:00
Jun YangandGitHub ce0280ca9f Merge pull request #235 from harttle/all-contributors/add-TrySound
docs: add TrySound as a contributor
2020-06-25 20:10:15 +08:00
allcontributors[bot]andGitHub 1066ef0f76 docs: update .all-contributorsrc [skip ci] 2020-06-25 12:09:50 +00:00
allcontributors[bot]andGitHub 7b38c28d52 docs: update README.md [skip ci] 2020-06-25 12:09:49 +00:00
Jun YangandGitHub 97e7983bd8 Merge pull request #234 from TrySound/proper-modules
feat: setup universal browser and node builds
2020-06-25 20:08:47 +08:00
Bogdan Chadkin 6cf6ffae90 feat: setup universal browser and node builds
In this diff I fixed es modules distribution for bundlers.

Before we had two problems
- bundlers use "module" field to find es module build
- bundlers cannot use provided es module with node target

In this diff I addressed both of them. Now "main" and "module" fields
points to "node" specific versions. "browser" field provides aliases
with browser compatible builds.

All modern browsers support both "module" and "browser" fields.
2020-06-25 13:30:56 +03:00
87 changed files with 6114 additions and 680 deletions
+55
View File
@@ -287,6 +287,61 @@
"contributions": [
"doc"
]
},
{
"login": "TrySound",
"name": "Bogdan Chadkin",
"avatar_url": "https://avatars0.githubusercontent.com/u/5635476?v=4",
"profile": "https://twitter.com/IAmTrySound",
"contributions": [
"code"
]
},
{
"login": "tejasmanohar",
"name": "Tejas Manohar",
"avatar_url": "https://avatars0.githubusercontent.com/u/5959235?v=4",
"profile": "https://hightouch.io",
"contributions": [
"code"
]
},
{
"login": "pdehaan",
"name": "Peter deHaan",
"avatar_url": "https://avatars2.githubusercontent.com/u/557895?v=4",
"profile": "http://about.me/peterdehaan",
"contributions": [
"doc"
]
},
{
"login": "amit777",
"name": "amit777",
"avatar_url": "https://avatars0.githubusercontent.com/u/2703309?v=4",
"profile": "https://github.com/amit777",
"contributions": [
"code",
"financial"
]
},
{
"login": "sschuldenzucker",
"name": "Steffen Schuldenzucker",
"avatar_url": "https://avatars3.githubusercontent.com/u/1100776?v=4",
"profile": "http://www.ifi.uzh.ch/en/ce/people/schuldenzucker.html",
"contributions": [
"code"
]
},
{
"login": "Pixcell",
"name": "Pixcell",
"avatar_url": "https://avatars0.githubusercontent.com/u/4005291?v=4",
"profile": "https://github.com/Pixcell",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,
+83
View File
@@ -1,3 +1,86 @@
# [9.19.0](https://github.com/harttle/liquidjs/compare/v9.18.0...v9.19.0) (2020-12-18)
### Bug Fixes
* lint ([de32259](https://github.com/harttle/liquidjs/commit/de32259))
* move offset adding complexity inside TimezoneDate ([26b2175](https://github.com/harttle/liquidjs/commit/26b2175))
* simpler timezone regex and non-null offset ([e3ecfe3](https://github.com/harttle/liquidjs/commit/e3ecfe3))
### Features
* add preserveTimezones option ([d70cd2a](https://github.com/harttle/liquidjs/commit/d70cd2a))
* parse and handle date timezone offsets ([c16c787](https://github.com/harttle/liquidjs/commit/c16c787))
# [9.18.0](https://github.com/harttle/liquidjs/compare/v9.17.0...v9.18.0) (2020-12-17)
### Bug Fixes
* address refactor comments ([6a0ad10](https://github.com/harttle/liquidjs/commit/6a0ad10))
### Features
* add option for keeping variable type in output ([cd92e77](https://github.com/harttle/liquidjs/commit/cd92e77))
# [9.17.0](https://github.com/harttle/liquidjs/compare/v9.16.1...v9.17.0) (2020-12-07)
### Bug Fixes
* elsif is not supported for unless, fixes [#268](https://github.com/harttle/liquidjs/issues/268) ([2bbf501](https://github.com/harttle/liquidjs/commit/2bbf501))
* enforce string-type pattern in `replace`, fixes [#243](https://github.com/harttle/liquidjs/issues/243) ([c8afa39](https://github.com/harttle/liquidjs/commit/c8afa39))
* raw block not ignoring {% characters, fixes [#263](https://github.com/harttle/liquidjs/issues/263) ([a492d8e](https://github.com/harttle/liquidjs/commit/a492d8e))
### Features
* passing liquid to FilterImpl, closes [#277](https://github.com/harttle/liquidjs/issues/277) ([f9f595f](https://github.com/harttle/liquidjs/commit/f9f595f))
## [9.16.1](https://github.com/harttle/liquidjs/compare/v9.16.0...v9.16.1) (2020-10-09)
### Bug Fixes
* braced property access ([18a807e](https://github.com/harttle/liquidjs/commit/18a807e))
# [9.16.0](https://github.com/harttle/liquidjs/compare/v9.15.1...v9.16.0) (2020-10-08)
### Features
* support jsTruthy, [#255](https://github.com/harttle/liquidjs/issues/255) [#257](https://github.com/harttle/liquidjs/issues/257) ([72ee7b4](https://github.com/harttle/liquidjs/commit/72ee7b4))
## [9.15.1](https://github.com/harttle/liquidjs/compare/v9.15.0...v9.15.1) (2020-10-03)
### Bug Fixes
* allow quoted variable name in capture, fixes [#252](https://github.com/harttle/liquidjs/issues/252) ([5b3f419](https://github.com/harttle/liquidjs/commit/5b3f419))
# [9.15.0](https://github.com/harttle/liquidjs/compare/v9.14.1...v9.15.0) (2020-08-04)
### Features
* export toPromise and toValue, see [#158](https://github.com/harttle/liquidjs/issues/158) ([2e5ab98](https://github.com/harttle/liquidjs/commit/2e5ab98))
## [9.14.1](https://github.com/harttle/liquidjs/compare/v9.14.0...v9.14.1) (2020-07-08)
### Bug Fixes
* enumerate Promises (e.g. in for & tablerow) ([#237](https://github.com/harttle/liquidjs/issues/237)) ([941dd66](https://github.com/harttle/liquidjs/commit/941dd66))
# [9.14.0](https://github.com/harttle/liquidjs/compare/v9.13.0...v9.14.0) (2020-06-25)
### Features
* setup universal browser and node builds ([6cf6ffa](https://github.com/harttle/liquidjs/commit/6cf6ffa))
# [9.13.0](https://github.com/harttle/liquidjs/compare/v9.12.0...v9.13.0) (2020-06-25)
+38 -30
View File
@@ -59,44 +59,52 @@ Want to contribute? see [Contribution Guidelines][contribution]. Thanks goes to
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="https://harttle.land"><img src="https://avatars3.githubusercontent.com/u/4427974?v=4" width="100px;" alt="Jun Yang"/><br /><sub><b>Jun Yang</b></sub></a><br /><a href="#maintenance-harttle" title="Maintenance">🚧</a> <a href="https://github.com/harttle/liquidjs/commits?author=harttle" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/chenos"><img src="https://avatars0.githubusercontent.com/u/2993310?v=4" width="100px;" alt="chenos"/><br /><sub><b>chenos</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=chenos" title="Code">💻</a></td>
<td align="center"><a href="https://zachleat.com/"><img src="https://avatars2.githubusercontent.com/u/39355?v=4" width="100px;" alt="Zach Leatherman"/><br /><sub><b>Zach Leatherman</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/issues?q=author%3Azachleat" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/thardy"><img src="https://avatars3.githubusercontent.com/u/120636?v=4" width="100px;" alt="Tim Hardy"/><br /><sub><b>Tim Hardy</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=thardy" title="Code">💻</a></td>
<td align="center"><a href="https://paulrobertlloyd.com/"><img src="https://avatars3.githubusercontent.com/u/813383?v=4" width="100px;" alt="Paul Robert Lloyd"/><br /><sub><b>Paul Robert Lloyd</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=paulrobertlloyd" title="Code">💻</a> <a href="https://github.com/harttle/liquidjs/issues?q=author%3Apaulrobertlloyd" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://twitter.com/alecdotbiz"><img src="https://avatars2.githubusercontent.com/u/1925840?v=4" width="100px;" alt="Alec Larson"/><br /><sub><b>Alec Larson</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=aleclarson" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/pmalouin"><img src="https://avatars1.githubusercontent.com/u/1411117?v=4" width="100px;" alt="Patrick Malouin"/><br /><sub><b>Patrick Malouin</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=pmalouin" title="Code">💻</a> <a href="https://github.com/harttle/liquidjs/commits?author=pmalouin" title="Documentation">📖</a></td>
<td align="center"><a href="https://harttle.land"><img src="https://avatars3.githubusercontent.com/u/4427974?v=4" width="100px;" alt=""/><br /><sub><b>Jun Yang</b></sub></a><br /><a href="#maintenance-harttle" title="Maintenance">🚧</a> <a href="https://github.com/harttle/liquidjs/commits?author=harttle" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/chenos"><img src="https://avatars0.githubusercontent.com/u/2993310?v=4" width="100px;" alt=""/><br /><sub><b>chenos</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=chenos" title="Code">💻</a></td>
<td align="center"><a href="https://zachleat.com/"><img src="https://avatars2.githubusercontent.com/u/39355?v=4" width="100px;" alt=""/><br /><sub><b>Zach Leatherman</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/issues?q=author%3Azachleat" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/thardy"><img src="https://avatars3.githubusercontent.com/u/120636?v=4" width="100px;" alt=""/><br /><sub><b>Tim Hardy</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=thardy" title="Code">💻</a></td>
<td align="center"><a href="https://paulrobertlloyd.com/"><img src="https://avatars3.githubusercontent.com/u/813383?v=4" width="100px;" alt=""/><br /><sub><b>Paul Robert Lloyd</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=paulrobertlloyd" title="Code">💻</a> <a href="https://github.com/harttle/liquidjs/issues?q=author%3Apaulrobertlloyd" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://twitter.com/alecdotbiz"><img src="https://avatars2.githubusercontent.com/u/1925840?v=4" width="100px;" alt=""/><br /><sub><b>Alec Larson</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=aleclarson" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/pmalouin"><img src="https://avatars1.githubusercontent.com/u/1411117?v=4" width="100px;" alt=""/><br /><sub><b>Patrick Malouin</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=pmalouin" title="Code">💻</a> <a href="https://github.com/harttle/liquidjs/commits?author=pmalouin" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://jaswrks.com"><img src="https://avatars3.githubusercontent.com/u/1563559?v=4" width="100px;" alt="jaswrks"/><br /><sub><b>jaswrks</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=jaswrks" title="Code">💻</a></td>
<td align="center"><a href="https://oott123.com"><img src="https://avatars2.githubusercontent.com/u/905663?v=4" width="100px;" alt="三三"/><br /><sub><b>三三</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=oott123" title="Code">💻</a> <a href="#ideas-oott123" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/ssendev"><img src="https://avatars0.githubusercontent.com/u/450793?v=4" width="100px;" alt="ssendev"/><br /><sub><b>ssendev</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=ssendev" title="Code">💻</a> <a href="https://github.com/harttle/liquidjs/commits?author=ssendev" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/wojtask9"><img src="https://avatars3.githubusercontent.com/u/6099236?v=4" width="100px;" alt="wojtask9"/><br /><sub><b>wojtask9</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=wojtask9" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/thelornenelson"><img src="https://avatars3.githubusercontent.com/u/24596583?v=4" width="100px;" alt="Andrew Barclay"/><br /><sub><b>Andrew Barclay</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=thelornenelson" title="Code">💻</a></td>
<td align="center"><a href="https://www.stam.pr/"><img src="https://avatars2.githubusercontent.com/u/142338?v=4" width="100px;" alt="Cory Mawhorter"/><br /><sub><b>Cory Mawhorter</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=cmawhorter" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/thehappybug"><img src="https://avatars0.githubusercontent.com/u/3393530?v=4" width="100px;" alt="Mehdi Jaffery"/><br /><sub><b>Mehdi Jaffery</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=thehappybug" title="Code">💻</a></td>
<td align="center"><a href="https://jaswrks.com"><img src="https://avatars3.githubusercontent.com/u/1563559?v=4" width="100px;" alt=""/><br /><sub><b>jaswrks</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=jaswrks" title="Code">💻</a></td>
<td align="center"><a href="https://oott123.com"><img src="https://avatars2.githubusercontent.com/u/905663?v=4" width="100px;" alt=""/><br /><sub><b>三三</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=oott123" title="Code">💻</a> <a href="#ideas-oott123" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/ssendev"><img src="https://avatars0.githubusercontent.com/u/450793?v=4" width="100px;" alt=""/><br /><sub><b>ssendev</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=ssendev" title="Code">💻</a> <a href="https://github.com/harttle/liquidjs/commits?author=ssendev" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/wojtask9"><img src="https://avatars3.githubusercontent.com/u/6099236?v=4" width="100px;" alt=""/><br /><sub><b>wojtask9</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=wojtask9" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/thelornenelson"><img src="https://avatars3.githubusercontent.com/u/24596583?v=4" width="100px;" alt=""/><br /><sub><b>Andrew Barclay</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=thelornenelson" title="Code">💻</a></td>
<td align="center"><a href="https://www.stam.pr/"><img src="https://avatars2.githubusercontent.com/u/142338?v=4" width="100px;" alt=""/><br /><sub><b>Cory Mawhorter</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=cmawhorter" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/thehappybug"><img src="https://avatars0.githubusercontent.com/u/3393530?v=4" width="100px;" alt=""/><br /><sub><b>Mehdi Jaffery</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=thehappybug" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/robinbijlani"><img src="https://avatars0.githubusercontent.com/u/2503108?v=4" width="100px;" alt="Robin Bijlani"/><br /><sub><b>Robin Bijlani</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=robinbijlani" title="Code">💻</a> <a href="https://github.com/harttle/liquidjs/issues?q=author%3Arobinbijlani" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://www.rmkennedy.com"><img src="https://avatars3.githubusercontent.com/u/8356669?v=4" width="100px;" alt="Ryan Kennedy"/><br /><sub><b>Ryan Kennedy</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=ryaninvents" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/strax"><img src="https://avatars2.githubusercontent.com/u/587213?v=4" width="100px;" alt="Sami Kukkonen"/><br /><sub><b>Sami Kukkonen</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=strax" title="Code">💻</a></td>
<td align="center"><a href="https://ScottFreeCode.github.io/"><img src="https://avatars3.githubusercontent.com/u/16506071?v=4" width="100px;" alt="Scott Santucci"/><br /><sub><b>Scott Santucci</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=ScottFreeCode" title="Code">💻</a></td>
<td align="center"><a href="http://stevenrescigno.com"><img src="https://avatars3.githubusercontent.com/u/8505293?v=4" width="100px;" alt="Steven "/><br /><sub><b>Steven </b></sub></a><br /><a href="#example-stevenanthonyrevo" title="Examples">💡</a> <a href="https://github.com/harttle/liquidjs/commits?author=stevenanthonyrevo" title="Code">💻</a></td>
<td align="center"><a href="https://efcl.info/"><img src="https://avatars1.githubusercontent.com/u/19714?v=4" width="100px;" alt="azu"/><br /><sub><b>azu</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=azu" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/wyozi"><img src="https://avatars3.githubusercontent.com/u/4894573?v=4" width="100px;" alt="Joonas"/><br /><sub><b>Joonas</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=wyozi" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/robinbijlani"><img src="https://avatars0.githubusercontent.com/u/2503108?v=4" width="100px;" alt=""/><br /><sub><b>Robin Bijlani</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=robinbijlani" title="Code">💻</a> <a href="https://github.com/harttle/liquidjs/issues?q=author%3Arobinbijlani" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://www.rmkennedy.com"><img src="https://avatars3.githubusercontent.com/u/8356669?v=4" width="100px;" alt=""/><br /><sub><b>Ryan Kennedy</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=ryaninvents" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/strax"><img src="https://avatars2.githubusercontent.com/u/587213?v=4" width="100px;" alt=""/><br /><sub><b>Sami Kukkonen</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=strax" title="Code">💻</a></td>
<td align="center"><a href="https://ScottFreeCode.github.io/"><img src="https://avatars3.githubusercontent.com/u/16506071?v=4" width="100px;" alt=""/><br /><sub><b>Scott Santucci</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=ScottFreeCode" title="Code">💻</a></td>
<td align="center"><a href="http://stevenrescigno.com"><img src="https://avatars3.githubusercontent.com/u/8505293?v=4" width="100px;" alt=""/><br /><sub><b>Steven </b></sub></a><br /><a href="#example-stevenanthonyrevo" title="Examples">💡</a> <a href="https://github.com/harttle/liquidjs/commits?author=stevenanthonyrevo" title="Code">💻</a></td>
<td align="center"><a href="https://efcl.info/"><img src="https://avatars1.githubusercontent.com/u/19714?v=4" width="100px;" alt=""/><br /><sub><b>azu</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=azu" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/wyozi"><img src="https://avatars3.githubusercontent.com/u/4894573?v=4" width="100px;" alt=""/><br /><sub><b>Joonas</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=wyozi" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/jamelait"><img src="https://avatars1.githubusercontent.com/u/14369255?v=4" width="100px;" alt="Jamel A."/><br /><sub><b>Jamel A.</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=jamelait" title="Code">💻</a></td>
<td align="center"><a href="https://brandonpittman.net"><img src="https://avatars0.githubusercontent.com/u/967145?v=4" width="100px;" alt="Brandon Pittman"/><br /><sub><b>Brandon Pittman</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=brandonpittman" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/tgrandgent"><img src="https://avatars3.githubusercontent.com/u/17069042?v=4" width="100px;" alt="tgrandgent"/><br /><sub><b>tgrandgent</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=tgrandgent" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/mastodon0"><img src="https://avatars1.githubusercontent.com/u/7924332?v=4" width="100px;" alt="Martin Schuster"/><br /><sub><b>Martin Schuster</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=mastodon0" title="Code">💻</a></td>
<td align="center"><a href="http://js.chenlei.me"><img src="https://avatars0.githubusercontent.com/u/6339390?v=4" width="100px;" alt="Ray"/><br /><sub><b>Ray</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=richardo2016" title="Tests">⚠️</a> <a href="https://github.com/harttle/liquidjs/commits?author=richardo2016" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/CriGoT"><img src="https://avatars0.githubusercontent.com/u/1936786?v=4" width="100px;" alt="Cristofer Gonzales"/><br /><sub><b>Cristofer Gonzales</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=CriGoT" title="Code">💻</a></td>
<td align="center"><a href="https://www.raymondcamden.com"><img src="https://avatars3.githubusercontent.com/u/393660?v=4" width="100px;" alt="Raymond Camden"/><br /><sub><b>Raymond Camden</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=cfjedimaster" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/jamelait"><img src="https://avatars1.githubusercontent.com/u/14369255?v=4" width="100px;" alt=""/><br /><sub><b>Jamel A.</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=jamelait" title="Code">💻</a></td>
<td align="center"><a href="https://brandonpittman.net"><img src="https://avatars0.githubusercontent.com/u/967145?v=4" width="100px;" alt=""/><br /><sub><b>Brandon Pittman</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=brandonpittman" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/tgrandgent"><img src="https://avatars3.githubusercontent.com/u/17069042?v=4" width="100px;" alt=""/><br /><sub><b>tgrandgent</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=tgrandgent" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/mastodon0"><img src="https://avatars1.githubusercontent.com/u/7924332?v=4" width="100px;" alt=""/><br /><sub><b>Martin Schuster</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=mastodon0" title="Code">💻</a></td>
<td align="center"><a href="http://js.chenlei.me"><img src="https://avatars0.githubusercontent.com/u/6339390?v=4" width="100px;" alt=""/><br /><sub><b>Ray</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=richardo2016" title="Tests">⚠️</a> <a href="https://github.com/harttle/liquidjs/commits?author=richardo2016" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/CriGoT"><img src="https://avatars0.githubusercontent.com/u/1936786?v=4" width="100px;" alt=""/><br /><sub><b>Cristofer Gonzales</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=CriGoT" title="Code">💻</a></td>
<td align="center"><a href="https://www.raymondcamden.com"><img src="https://avatars3.githubusercontent.com/u/393660?v=4" width="100px;" alt=""/><br /><sub><b>Raymond Camden</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=cfjedimaster" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://stedman.dev"><img src="https://avatars1.githubusercontent.com/u/183122?v=4" width="100px;" alt="Steve Stedman"/><br /><sub><b>Steve Stedman</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=stedman" title="Documentation">📖</a></td>
<td align="center"><a href="https://ciccarello.me"><img src="https://avatars0.githubusercontent.com/u/11273838?v=4" width="100px;" alt="Anthony Ciccarello"/><br /><sub><b>Anthony Ciccarello</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=aciccarello" title="Documentation">📖</a></td>
<td align="center"><a href="https://stedman.dev"><img src="https://avatars1.githubusercontent.com/u/183122?v=4" width="100px;" alt=""/><br /><sub><b>Steve Stedman</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=stedman" title="Documentation">📖</a></td>
<td align="center"><a href="https://ciccarello.me"><img src="https://avatars0.githubusercontent.com/u/11273838?v=4" width="100px;" alt=""/><br /><sub><b>Anthony Ciccarello</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=aciccarello" title="Documentation">📖</a></td>
<td align="center"><a href="https://twitter.com/IAmTrySound"><img src="https://avatars0.githubusercontent.com/u/5635476?v=4" width="100px;" alt=""/><br /><sub><b>Bogdan Chadkin</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=TrySound" title="Code">💻</a></td>
<td align="center"><a href="https://hightouch.io"><img src="https://avatars0.githubusercontent.com/u/5959235?v=4" width="100px;" alt=""/><br /><sub><b>Tejas Manohar</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=tejasmanohar" title="Code">💻</a></td>
<td align="center"><a href="http://about.me/peterdehaan"><img src="https://avatars2.githubusercontent.com/u/557895?v=4" width="100px;" alt=""/><br /><sub><b>Peter deHaan</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=pdehaan" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/amit777"><img src="https://avatars0.githubusercontent.com/u/2703309?v=4" width="100px;" alt=""/><br /><sub><b>amit777</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=amit777" title="Code">💻</a> <a href="#financial-amit777" title="Financial">💵</a></td>
<td align="center"><a href="http://www.ifi.uzh.ch/en/ce/people/schuldenzucker.html"><img src="https://avatars3.githubusercontent.com/u/1100776?v=4" width="100px;" alt=""/><br /><sub><b>Steffen Schuldenzucker</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=sschuldenzucker" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/Pixcell"><img src="https://avatars0.githubusercontent.com/u/4005291?v=4" width="100px;" alt=""/><br /><sub><b>Pixcell</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=Pixcell" title="Code">💻</a></td>
</tr>
</table>
+13
View File
@@ -0,0 +1,13 @@
# Security Policy
## Supported Versions
Only the latest major version is supported with security updates. It can be changed if many people are relying on a specific version for some reason. If this is the case, welcome to file an issue.
## Reporting a Vulnerability
Please contact yangjvn@126.com to report a vulnerability.
- 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).
+9 -1
View File
@@ -9,8 +9,16 @@ npm run build
cp CNAME public/
cp ../CHANGELOG.md source/tutorials/changelog.md
sed -i \
-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' \
source/tutorials/changelog.md
cp source/tutorials/changelog.md source/zh-cn/tutorials/changelog.md
sed -i '1i ---\ntitle: Changelog\nauto: true\n---\n' source/tutorials/changelog.md
cp ../CHANGELOG.md source/zh-cn/tutorials/changelog.md
sed -i '1i ---\ntitle: 更新日志\nauto: true\n---\n' source/zh-cn/tutorials/changelog.md
if [ "$HEXO_ALGOLIA_INDEXING_KEY" != "" ]; then
+3 -3
View File
@@ -4,7 +4,7 @@ const isQuote = c => c === '"' || c === "'"
const isOperator = c => '!=<>'.includes(c)
const isNumber = c => c >= '0' && c <= '9'
const isCharacter = c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
const isVariable = c => '_-?'.includes(c) || isCharacter(c) || isNumber(c)
const isIdentifier = c => '_-?'.includes(c) || isCharacter(c) || isNumber(c)
const isBlank = c => c === '\n' || c === '\t' || c === ' ' || c === '\r' || c === '\v' || c === '\f'
const isInlineBlank = c => c === '\t' || c === ' ' || c === '\r'
const isSign = c => c === '-' || c === '+'
@@ -15,7 +15,7 @@ const types = []
for (let i = 0; i < 128; i++) {
const c = String.fromCharCode(i)
let n = 0
if (isVariable(c)) n |= 1
if (isIdentifier(c)) n |= 1
if (isOperator(c)) n |= 2
if (isBlank(c)) n |= 4
if (isQuote(c)) n |= 8
@@ -31,7 +31,7 @@ console.log(`
// This file is generated by bin/character-gen.js
// bitmask character types to boost performance
export const TYPES = [${types.join(', ')}]
export const VARIABLE = 1
export const IDENTIFIER = 1
export const OPERATOR = 2
export const BLANK = 4
export const QUOTE = 8
+9 -9
View File
@@ -2127,9 +2127,9 @@
}
},
"highlight.js": {
"version": "9.18.1",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.18.1.tgz",
"integrity": "sha512-OrVKYz70LHsnCgmbXctv/bfuvntIKDz177h0Co37DQ5jamGZLVmoCVMtjMtNZY3X9DrCcKfklHPNeA0uPZhSJg=="
"version": "9.18.5",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.18.5.tgz",
"integrity": "sha512-a5bFyofd/BHCX52/8i8uJkjr9DYwXIPnM/plwI6W7ezItLGqzt7X2G2nXuYSfsIJdkwwj/g9DG1LkcGJI/dDoA=="
},
"html-entities": {
"version": "1.2.1",
@@ -2528,9 +2528,9 @@
}
},
"lodash": {
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
"version": "4.17.19",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
},
"lodash.assignin": {
"version": "4.2.0",
@@ -3060,9 +3060,9 @@
"integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE="
},
"prismjs": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.19.0.tgz",
"integrity": "sha512-IVFtbW9mCWm9eOIaEkNyo2Vl4NnEifis2GQ7/MLRG5TQe6t+4Sj9J5QWI9i3v+SS43uZBlCAOn+zYTVYQcPXJw==",
"version": "1.21.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.21.0.tgz",
"integrity": "sha512-uGdSIu1nk3kej2iZsLyDoJ7e9bnPzIgY0naW/HdknGj61zScaprVEVGHrPoXqI+M9sP0NDnTK2jpkvmldpuqDw==",
"requires": {
"clipboard": "^2.0.0"
}
+8
View File
@@ -65,6 +65,12 @@ Before 2.0.1, <code>extname</code> is set to `.liquid` by default. To change tha
**globals** is used to define global variables available to all templates even in cases of [render tag][render]. See [3185][185] for details.
## jsTruthy
**jsTruthy** is used to use standard Javascript truthiness rather than the Shopify.
it defaults to false. For example, when set to true, a blank string would evaluate to false with jsTruthy. With Shopify's truthiness, a blank string is true.
## Trimming
**greedy**, **trimOutputLeft**, **trimOutputRight**, **trimTagLeft**, **trimTagRight** options are used to eliminate extra newlines and indents in templates arround Liquid Constructs. See [Whitespace Control][wc] for details.
@@ -83,6 +89,8 @@ Before 2.0.1, <code>extname</code> is set to `.liquid` by default. To change tha
**strictVariables** is used to assert variable existence. If set to `false`, undefined variables will be rendered as empty string. Otherwise, undefined variables will cause a render exception. Defaults to `false`.
**lenientIf** modifies the behavior of `strictVariables` to allow handling optional variables. If set to `true`, an undefined variable will *not* cause an exception in the following two situations: a) it is the condition to an `if`, `elsif`, or `unless` tag; b) it occurs right before a `default` filter. Irrelevant if `strictVariables` is not set. Defaults to `false`.
{% note info Non-existent Tags %}
Non-existent tags always throw errors during pasrsing and this behaviour can not be customized.
{% endnote %}
+4 -4
View File
@@ -42,12 +42,12 @@ Pre-built UMD bundles are also available:
```html
<!--for production-->
<script src="https://cdn.jsdelivr.net/npm/liquidjs/dist/liquid.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/liquidjs/dist/liquid.browser.min.js"></script>
<!--for development-->
<script src="https://cdn.jsdelivr.net/npm/liquidjs/dist/liquid.js"></script>
<script src="https://cdn.jsdelivr.net/npm/liquidjs/dist/liquid.browser.umd.js"></script>
```
{% note info Working Demo %} Here's a living demo on jsFiddle: <a href="https://jsfiddle.net/x43eb0z6/" target="_blank">jsfiddle.net/x43eb0z6</a>, and the source code is also available in <a href="https://github.com/harttle/liquidjs/blob/master/demo/browser/" target="_blank">liquidjs/demo/browser/</a>.{% endnote %}
{% note info Working Demo %} Here's a living demo on jsFiddle: <a href="https://jsfiddle.net/pd4jhzLs/1/" target="_blank">jsfiddle.net/pd4jhzLs/1/</a>, and the source code is also available in <a href="https://github.com/harttle/liquidjs/blob/master/demo/browser/" target="_blank">liquidjs/demo/browser/</a>.{% endnote %}
{% note warn Compatibility %} You may need a <a href="https://github.com/taylorhakes/promise-polyfill" target="_blank">Promise polyfill</a> for legacy browsers like IE and Android UC, see <a href="http://caniuse.com/#feat=promises" target="_blank">caniuse statistics</a>. {% endnote %}
@@ -69,4 +69,4 @@ echo 'Hello, {{ name }}.' | npx liquidjs '{"name": "Snake"}'
A ReactJS demo is also added by [@stevenanthonyrevo](https://github.com/stevenanthonyrevo), see [liquidjs/demo/reactjs/](https://github.com/harttle/liquidjs/blob/master/demo/reactjs/).
[intro]: ./intro-to-liquid.html
[intro]: ./intro-to-liquid.html
+20 -1
View File
@@ -17,11 +17,30 @@ value | truthy | falsy
`string` | ✔️ |
`empty string` | ✔️ |
`0` | ✔️ |
`integer` | ✔️ |
`integer` | ✔️ |
`float` | ✔️ |
`array` | ✔️ |
`empty array` | ✔️ |
## Use JavaScript Truthy
Note that liquidjs use Shopify's truthiness by default. But it can be toggled to used standard JavaScript truthiness by setting the **jsTruthy** option to `true`.
value | truthy | falsy
--- | --- | ---
`true` | ✔️ |
`false` | | ✔️
`null` | | ✔️
`undefined` | | ✔️
`string` | ✔️ |
`empty string` | | ✔️
`0` | | ✔️
`integer` | ✔️ |
`float` | ✔️ |
`array` | ✔️ |
`empty array` | ✔️ |
[ruby]: https://shopify.github.io/liquid
[sl]: https://www.npmjs.com/package/liquidjs
[diff]: https://github.com/harttle/liquidjs#differences-and-limitations
+6
View File
@@ -63,6 +63,12 @@ LiquidJS 把这个选项默认值设为 <code>true</code> 以兼容于 shopify/l
**globals** 用来定义对所有模板可见的全局变量。包括 [render tag][render] 引入的子模板,见 [3185][185]。
## jsTruthy
**jsTruthy** 用来使用 Javascript 的真值判断,默认为 `false` 使用 Shopify 方式。
例如,空字符串在 JavaScript 中为假(`jsTruthy``true` 时),在 Shopify 真值表中为真。
## 换行和缩进
**greedy**, **trimOutputLeft**, **trimOutputRight**, **trimTagLeft**, **trimTagRight** 选项用来移除 Liquid 语法周围的换行和缩进,详情请参考 [Whitespace Control][wc]。
@@ -8,7 +8,7 @@ title: 真和假
根据 [Shopify 的文档](https://shopify.github.io/liquid/basics/truthy-and-falsy/)Ruby 版本除了 `false``nil` 之外的所有值都是真,但 JavaScript 有完全不同的类型系统,比如我们有 `undefined` 类型,以及不区分 `integer``float`,因此有些不同:
value | truthy | falsy
| 真 | 假
--- | --- | ---
`true` | ✔️ |
`false` | | ✔️
@@ -22,6 +22,24 @@ value | truthy | falsy
`array` | ✔️ |
`empty array` | ✔️ |
## 使用 JavaScript 真值
liquidjs 默认使用 Shopify 的真值表,但可以通过设置 **jsTruthy** 选项为 `true` 来使用标准的 JavaScript 真值。
值 | 真 | 假
--- | --- | ---
`true` | ✔️ |
`false` | | ✔️
`null` | | ✔️
`undefined` | | ✔️
`string` | ✔️ |
`empty string` | | ✔️
`0` | | ✔️
`integer` | ✔️ |
`float` | ✔️ |
`array` | ✔️ |
`empty array` | ✔️ |
[ruby]: https://shopify.github.io/liquid
[sl]: https://www.npmjs.com/package/liquidjs
[diff]: https://github.com/harttle/liquidjs#differences-and-limitations
+3 -3
View File
@@ -28,9 +28,9 @@
<ul id="intro-cmd-wrap">
<li class="intro-code-item"><span class="hl">import</span> { Liquid } <span class="hl">from</span> 'liquidjs'</li>
<li class="intro-code-item"><span class="hl">const</span> engine = <span class="hl">new</span> Liquid()</li>
<li class="intro-code-item"><span class="hl">const</span> tpl = engine.parse(<span class="hl-str">'Welcome to &#x7b;&#x7b;v}}!'</span>))</li>
<li class="intro-code-item"><span class="hl">const</span> html = <span class="hl">await</span> engine.render(tpl, {v: "Liquid"})</li>
<li class="intro-code-item"><span class="hl-comment">// html === "Welcome to Liquid!"</span></li>
<li class="intro-code-item"><span class="hl">const</span> tpl = engine.parse(<span class="hl-str">'Welcome to &#x7b;&#x7b;v}}!'</span>)</li>
<li class="intro-code-item"><span class="hl">const</span> engine.render(tpl, {v: "Liquid"}).then(console.log)</li>
<li class="intro-code-item"><span class="hl-comment">// Output: "Welcome to Liquid!"</span></li>
</ul>
<div id="intro-get-started-wrap">
<a href="tutorials/setup.html" id="intro-get-started-link">{{ __('index.get_started') }}</a>
+2 -4
View File
@@ -50,10 +50,8 @@
background: color-link-hover
#banner-share
display: none
display: block
padding-bottom: 60px
@media mq-normal
display: block
#intro-news-list
background-color: darken(color-navy, 10%)
@@ -178,7 +176,7 @@
transition: 0.2s
color: color-link
&:hover
background: @border-color
background: lighten(color-link, 20%)
color: #fff
#sponsors-wrap, #contributors-wrap
+17 -2
View File
@@ -66,9 +66,8 @@
preview.renderer.setShowGutter(false);
preview.renderer.setPadding(20);
editor.setValue(sourceEl.textContent, 1);
editor.setValue(getInitialValue(), 1);
editor.on('change', update);
editor.focus();
update();
function createEditor(id, lang) {
@@ -83,8 +82,16 @@
return editor;
}
function getInitialValue() {
try {
return atou(location.hash.slice(1)) || sourceEl.textContent
} catch (e) {}
return sourceEl.textContent
}
async function update() {
const tpl = editor.getValue();
history.replaceState({}, '', '#' + utoa(tpl));
try {
const html = await engine.parseAndRender(tpl);
preview.setValue(html, 1);
@@ -93,6 +100,14 @@
throw err;
}
}
function atou(str) {
return decodeURIComponent(escape(atob(str)))
}
function utoa(str) {
return btoa(unescape(encodeURIComponent(str)))
}
}());
if ('serviceWorker' in navigator) {
View File
+4991 -398
View File
File diff suppressed because it is too large Load Diff
+11 -7
View File
@@ -1,13 +1,17 @@
{
"name": "liquidjs",
"version": "9.13.0",
"version": "9.19.0",
"description": "A simple, expressive, safe and Shopify compatible template engine in pure JavaScript.",
"main": "dist/liquid.cjs.js",
"es2015": "dist/liquid.esm.js",
"browser": "dist/liquid.js",
"main": "dist/liquid.node.cjs.js",
"module": "dist/liquid.node.esm.js",
"es2015": "dist/liquid.browser.esm.js",
"browser": {
"./dist/liquid.node.cjs.js": "./dist/liquid.browser.umd.js",
"./dist/liquid.node.esm.js": "./dist/liquid.browser.esm.js"
},
"types": "dist/liquid.d.ts",
"scripts": {
"lint": "eslint .",
"lint": "eslint '**/*.ts' .",
"check": "npm test && npm run lint",
"unit": "mocha \"test/unit/**/*.ts\"",
"integration": "mocha \"test/integration/**/*.ts\"",
@@ -16,7 +20,7 @@
"benchmark": "node --expose-gc benchmark/index",
"coverage": "nyc --reporter=html mocha \"test/{unit,integration}/**/*.ts\"",
"coverage-coveralls": "nyc mocha \"test/{unit,integration}/**/*.ts\" && nyc report --reporter=text-lcov | coveralls",
"build": "rollup -c rollup.config.ts && ls -lh dist",
"build": "rm -rf dist && rollup -c rollup.config.ts && ls -lh dist",
"build-docs": "bin/build-docs.sh",
"watch": "tsc --watch"
},
@@ -96,7 +100,7 @@
"rollup-plugin-replace": "^2.1.0",
"rollup-plugin-typescript2": "^0.21.1",
"rollup-plugin-uglify": "^6.0.2",
"semantic-release": "^15.13.19",
"semantic-release": "^17.2.3",
"sinon": "^7.5.0",
"sinon-chai": "^3.3.0",
"supertest": "^3.4.2",
+34 -13
View File
@@ -15,9 +15,9 @@ const treeshake = {
}
const input = './src/liquid.ts'
const cjs = {
const nodeCjs = {
output: [{
file: 'dist/liquid.cjs.js',
file: 'dist/liquid.node.cjs.js',
format: 'cjs',
banner
}],
@@ -36,9 +36,30 @@ const cjs = {
input
}
const esm = {
const nodeEsm = {
output: [{
file: 'dist/liquid.esm.js',
file: 'dist/liquid.node.esm.js',
format: 'esm',
banner
}],
external: ['path', 'fs'],
plugins: [typescript({
tsconfigOverride: {
include: [ 'src' ],
exclude: [ 'test', 'benchmark' ],
compilerOptions: {
target: 'ES2017',
module: 'ES2015'
}
}
})],
treeshake,
input
}
const browserEsm = {
output: [{
file: 'dist/liquid.browser.esm.js',
format: 'esm',
banner
}],
@@ -64,9 +85,9 @@ const esm = {
input
}
const umd = {
const browserUmd = {
output: [{
file: 'dist/liquid.js',
file: 'dist/liquid.browser.umd.js',
name: 'liquidjs',
format: 'umd',
sourcemap,
@@ -93,9 +114,9 @@ const umd = {
input
}
const min = {
const browserMin = {
output: [{
file: 'dist/liquid.min.js',
file: 'dist/liquid.browser.min.js',
name: 'liquidjs',
format: 'umd',
sourcemap
@@ -124,10 +145,10 @@ const min = {
const bundles = []
const env = process.env.BUNDLES || ''
if (env.includes('cjs')) bundles.push(cjs)
if (env.includes('esm')) bundles.push(esm)
if (env.includes('umd')) bundles.push(umd)
if (env.includes('min')) bundles.push(min)
if (bundles.length === 0) bundles.push(cjs, umd, min, esm)
if (env.includes('cjs')) bundles.push(nodeCjs)
if (env.includes('esm')) bundles.push(nodeEsm, browserEsm)
if (env.includes('umd')) bundles.push(browserUmd)
if (env.includes('min')) bundles.push(browserMin)
if (bundles.length === 0) bundles.push(nodeCjs, nodeEsm, browserEsm, browserUmd, browserMin)
export default bundles
+1 -1
View File
@@ -36,7 +36,7 @@ export function slice<T> (v: T[], begin: number, length = 1): T[] {
export function where<T extends object> (this: FilterImpl, arr: T[], property: string, expected?: any): T[] {
return toArray(arr).filter(obj => {
const value = this.context.getFromScope(obj, String(property).split('.'))
return expected === undefined ? isTruthy(value) : value === expected
return expected === undefined ? isTruthy(value, this.context) : value === expected
})
}
+10 -3
View File
@@ -1,14 +1,21 @@
import strftime from '../../util/strftime'
import strftime, { TimezoneDate } from '../../util/strftime'
import { isString, isNumber } from '../../util/underscore'
import { FilterImpl } from '../../template/filter/filter-impl'
export function date (v: string | Date, arg: string) {
export function date (this: FilterImpl, v: string | Date, arg: string) {
let date = v
if (v === 'now' || v === 'today') {
date = new Date()
} else if (isNumber(v)) {
date = new Date(v * 1000)
} else if (isString(v)) {
date = /^\d+$/.test(v) ? new Date(+v * 1000) : new Date(v)
if (/^\d+$/.test(v)) {
date = new Date(+v * 1000)
} else if (this.context.opts.preserveTimezones) {
date = new TimezoneDate(v)
} else {
date = new Date(v)
}
}
return isValidDate(date) ? strftime(date, arg) : v
}
+3 -2
View File
@@ -1,9 +1,10 @@
import { isFalsy } from '../../render/boolean'
import { isArray, isString, toValue } from '../../util/underscore'
import { FilterImpl } from '../../template/filter/filter-impl'
export function Default<T1, T2> (v: string | T1, arg: T2): string | T1 | T2 {
export function Default<T1, T2> (this: FilterImpl, v: string | T1, arg: T2): string | T1 | T2 {
if (isArray(v) || isString(v)) return v.length ? v : arg
return isFalsy(toValue(v)) ? arg : v
return isFalsy(toValue(v), this.context) ? arg : v
}
export function json (v: any) {
return JSON.stringify(v)
+5 -5
View File
@@ -29,11 +29,11 @@ export function upcase (str: string) {
}
export function remove (v: string, arg: string) {
return stringify(v).split(arg).join('')
return stringify(v).split(String(arg)).join('')
}
export function removeFirst (v: string, l: string) {
return stringify(v).replace(l, '')
return stringify(v).replace(String(l), '')
}
export function rstrip (str: string) {
@@ -41,7 +41,7 @@ export function rstrip (str: string) {
}
export function split (v: string, arg: string) {
return stringify(v).split(arg)
return stringify(v).split(String(arg))
}
export function strip (v: string) {
@@ -58,11 +58,11 @@ export function capitalize (str: string) {
}
export function replace (v: string, pattern: string, replacement: string) {
return stringify(v).split(pattern).join(replacement)
return stringify(v).split(String(pattern)).join(replacement)
}
export function replaceFirst (v: string, arg1: string, arg2: string) {
return stringify(v).replace(arg1, arg2)
return stringify(v).replace(String(arg1), arg2)
}
export function truncate (v: string, l = 50, o = '...') {
+1 -1
View File
@@ -3,7 +3,7 @@ import { Tokenizer, assert, TagImplOptions, TagToken, Context } from '../../type
export default {
parse: function (token: TagToken) {
const tokenizer = new Tokenizer(token.args)
this.key = tokenizer.readWord().content
this.key = tokenizer.readIdentifier().content
tokenizer.skipBlank()
assert(tokenizer.peek() === '=', () => `illegal token ${token.getText()}`)
tokenizer.advance()
+9 -1
View File
@@ -1,9 +1,10 @@
import { Tokenizer, assert, Template, Context, TagImplOptions, TagToken, TopLevelToken } from '../../types'
import { evalQuotedToken } from '../../render/expression'
export default {
parse: function (tagToken: TagToken, remainTokens: TopLevelToken[]) {
const tokenizer = new Tokenizer(tagToken.args)
this.variable = tokenizer.readWord().content
this.variable = readVariableName(tokenizer)
assert(this.variable, () => `${tagToken.args} not valid identifier`)
this.templates = []
@@ -22,3 +23,10 @@ export default {
ctx.bottom()[this.variable] = html
}
} as TagImplOptions
function readVariableName (tokenizer: Tokenizer) {
const word = tokenizer.readIdentifier().content
if (word) return word
const quoted = tokenizer.readQuoted()
if (quoted) return evalQuotedToken(quoted)
}
+1 -1
View File
@@ -4,7 +4,7 @@ import { isNumber, stringify } from '../../util/underscore'
export default {
parse: function (token: TagToken) {
const tokenizer = new Tokenizer(token.args)
this.variable = tokenizer.readWord().content
this.variable = tokenizer.readIdentifier().content
},
render: function (context: Context, emitter: Emitter) {
const scope = context.environments
+3 -3
View File
@@ -8,8 +8,8 @@ export default {
parse: function (token: TagToken, remainTokens: TopLevelToken[]) {
const toknenizer = new Tokenizer(token.args)
const variable = toknenizer.readWord()
const inStr = toknenizer.readWord()
const variable = toknenizer.readIdentifier()
const inStr = toknenizer.readIdentifier()
const collection = toknenizer.readValue()
assert(
variable.size() && inStr.content === 'in' && collection,
@@ -36,7 +36,7 @@ export default {
},
render: function * (ctx: Context, emitter: Emitter) {
const r = this.liquid.renderer
let collection = toEnumerable(evalToken(this.collection, ctx))
let collection = toEnumerable(yield evalToken(this.collection, ctx))
if (!collection.length) {
yield r.renderTemplates(this.elseTemplates, ctx, emitter)
+2 -2
View File
@@ -31,8 +31,8 @@ export default {
const r = this.liquid.renderer
for (const branch of this.branches) {
const cond = yield new Expression(branch.cond).value(ctx)
if (isTruthy(cond)) {
const cond = yield new Expression(branch.cond, ctx.opts.lenientIf).value(ctx)
if (isTruthy(cond, ctx)) {
yield r.renderTemplates(branch.templates, ctx, emitter)
return
}
+1 -1
View File
@@ -11,7 +11,7 @@ export default {
assert(this.file, () => `illegal argument "${token.args}"`)
const begin = tokenizer.p
const withStr = tokenizer.readWord()
const withStr = tokenizer.readIdentifier()
if (withStr.content === 'with') {
tokenizer.skipBlank()
if (tokenizer.peek() !== ':') {
+1 -1
View File
@@ -4,7 +4,7 @@ import { Tokenizer, Emitter, TagToken, Context, TagImplOptions } from '../../typ
export default {
parse: function (token: TagToken) {
const tokenizer = new Tokenizer(token.args)
this.variable = tokenizer.readWord().content
this.variable = tokenizer.readIdentifier().content
},
render: function (context: Context, emitter: Emitter) {
const scope = context.environments
+3 -3
View File
@@ -15,16 +15,16 @@ export default {
while (!tokenizer.end()) {
tokenizer.skipBlank()
const begin = tokenizer.p
const keyword = tokenizer.readWord()
const keyword = tokenizer.readIdentifier()
if (keyword.content === 'with' || keyword.content === 'for') {
tokenizer.skipBlank()
if (tokenizer.peek() !== ':') {
const value = tokenizer.readValue()
if (value) {
const beforeAs = tokenizer.p
const asStr = tokenizer.readWord()
const asStr = tokenizer.readIdentifier()
let alias
if (asStr.content === 'as') alias = tokenizer.readWord()
if (asStr.content === 'as') alias = tokenizer.readIdentifier()
else tokenizer.p = beforeAs
this[keyword.content] = { value, alias: alias && alias.content }
+3 -3
View File
@@ -7,10 +7,10 @@ export default {
parse: function (tagToken: TagToken, remainTokens: TopLevelToken[]) {
const tokenizer = new Tokenizer(tagToken.args)
this.variable = tokenizer.readWord()
this.variable = tokenizer.readIdentifier()
tokenizer.skipBlank()
const tmp = tokenizer.readWord()
const tmp = tokenizer.readIdentifier()
assert(tmp && tmp.content === 'in', () => `illegal tag: ${tagToken.getText()}`)
this.collection = tokenizer.readValue()
@@ -30,7 +30,7 @@ export default {
},
render: function * (ctx: Context, emitter: Emitter) {
let collection = toEnumerable(evalToken(this.collection, ctx))
let collection = toEnumerable(yield evalToken(this.collection, ctx))
const hash = yield this.hash.render(ctx)
const offset = hash.offset || 0
const limit = (hash.limit === undefined) ? collection.length : hash.limit
+24 -5
View File
@@ -1,8 +1,9 @@
import { TopLevelToken, Template, Emitter, Expression, isFalsy, ParseStream, Context, TagImplOptions, Token, TagToken } from '../../types'
import { TopLevelToken, Template, Emitter, Expression, isTruthy, isFalsy, ParseStream, Context, TagImplOptions, TagToken } from '../../types'
export default {
parse: function (tagToken: TagToken, remainTokens: TopLevelToken[]) {
this.templates = []
this.branches = []
this.elseTemplates = []
let p
const stream: ParseStream = this.liquid.parser.parseStream(remainTokens)
@@ -10,6 +11,12 @@ export default {
p = this.templates
this.cond = tagToken.args
})
.on('tag:elsif', (token: TagToken) => {
this.branches.push({
cond: token.args,
templates: p = []
})
})
.on('tag:else', () => (p = this.elseTemplates))
.on('tag:endunless', () => stream.stop())
.on('template', (tpl: Template) => p.push(tpl))
@@ -22,9 +29,21 @@ export default {
render: function * (ctx: Context, emitter: Emitter) {
const r = this.liquid.renderer
const cond = yield new Expression(this.cond).value(ctx)
yield (isFalsy(cond)
? r.renderTemplates(this.templates, ctx, emitter)
: r.renderTemplates(this.elseTemplates, ctx, emitter))
const cond = yield new Expression(this.cond, ctx.opts.lenientIf).value(ctx)
if (isFalsy(cond, ctx)) {
yield r.renderTemplates(this.templates, ctx, emitter)
return
}
for (const branch of this.branches) {
const cond = yield new Expression(branch.cond, ctx.opts.lenientIf).value(ctx)
if (isTruthy(cond, ctx)) {
yield r.renderTemplates(branch.templates, ctx, emitter)
return
}
}
yield r.renderTemplates(this.elseTemplates, ctx, emitter)
}
} as TagImplOptions
+2 -1
View File
@@ -3,6 +3,7 @@ import { __assign } from 'tslib'
import { NormalizedFullOptions, defaultOptions } from '../liquid-options'
import { Scope } from './scope'
import { isArray, isNil, isString, isFunction, toLiquid } from '../util/underscore'
import { InternalUndefinedVariableError } from '../util/error'
export class Context {
private scopes: Scope[] = [{}]
@@ -42,7 +43,7 @@ export class Context {
return paths.reduce((scope, path) => {
scope = readProperty(scope, path)
if (isNil(scope) && this.opts.strictVariables) {
throw new TypeError(`undefined variable: ${path}`)
throw new InternalUndefinedVariableError(path)
}
return scope
}, scope)
+17 -1
View File
@@ -11,12 +11,16 @@ export interface LiquidOptions {
extname?: string;
/** Whether or not to cache resolved templates. Defaults to `false`. */
cache?: boolean | number | Cache<Template[]>;
/** 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`. */
dynamicPartials?: boolean;
/** Whether or not to assert filter existence. If set to `false`, undefined filters will be skipped. Otherwise, undefined filters will cause an exception. Defaults to `false`. */
strictFilters?: boolean;
/** Whether or not to assert variable existence. If set to `false`, undefined variables will be rendered as empty string. Otherwise, undefined variables will cause an exception. Defaults to `false`. */
strictVariables?: boolean;
/** Modifies the behavior of `strictVariables`. If set, a single undefined variable will *not* cause an exception in the context of the `if`/`elsif`/`unless` tag and the `default` filter. Instead, it will evaluate to `false` and `null`, respectively. Irrelevant if `strictVariables` is not set. Defaults to `false`. **/
lenientIf?: boolean;
/** Strip blank characters (including ` `, `\t`, and `\r`) from the right of tags (`{% %}`) until `\n` (inclusive). Defaults to `false`. */
trimTagRight?: boolean;
/** Similar to `trimTagRight`, whereas the `\n` is exclusive. Defaults to `false`. See Whitespace Control for details. */
@@ -33,12 +37,16 @@ export interface LiquidOptions {
outputDelimiterLeft?: string;
/** The right delimiter for liquid outputs. **/
outputDelimiterRight?: string;
/** Whether input strings to date filter preserve the given timezone **/
preserveTimezones?: boolean;
/** Whether `trim*Left`/`trim*Right` is greedy. When set to `true`, all consecutive blank characters including `\n` will be trimed regardless of line breaks. Defaults to `true`. */
greedy?: boolean;
/** `fs` is used to override the default file-system module with a custom implementation. */
fs?: FS;
/** the global environment passed down to all partial templates, i.e. templates included by `include`, `layout` and `render` tags. */
globals?: object;
/** Whether or not to keep value type when writing the Output. Defaults to `false`. */
keepOutputType?: boolean;
}
interface NormalizedOptions extends LiquidOptions {
@@ -50,9 +58,11 @@ export interface NormalizedFullOptions extends NormalizedOptions {
root: string[];
extname: string;
cache: undefined | Cache<Template[]>;
jsTruthy: boolean;
dynamicPartials: boolean;
strictFilters: boolean;
strictVariables: boolean;
lenientIf: boolean;
trimTagRight: boolean;
trimTagLeft: boolean;
trimOutputRight: boolean;
@@ -61,8 +71,10 @@ export interface NormalizedFullOptions extends NormalizedOptions {
tagDelimiterRight: string;
outputDelimiterLeft: string;
outputDelimiterRight: string;
preserveTimezones: boolean;
greedy: boolean;
globals: object;
keepOutputType: boolean;
}
export const defaultOptions: NormalizedFullOptions = {
@@ -70,6 +82,7 @@ export const defaultOptions: NormalizedFullOptions = {
cache: undefined,
extname: '',
dynamicPartials: true,
jsTruthy: false,
trimTagRight: false,
trimTagLeft: false,
trimOutputRight: false,
@@ -79,9 +92,12 @@ export const defaultOptions: NormalizedFullOptions = {
tagDelimiterRight: '%}',
outputDelimiterLeft: '{{',
outputDelimiterRight: '}}',
preserveTimezones: false,
strictFilters: false,
strictVariables: false,
globals: {}
lenientIf: false,
globals: {},
keepOutputType: false
}
export function normalize (options?: LiquidOptions): NormalizedOptions {
+16 -14
View File
@@ -14,7 +14,8 @@ import { FilterMap } from './template/filter/filter-map'
import { LiquidOptions, normalizeStringArray, NormalizedFullOptions, applyDefault, normalize } from './liquid-options'
import { FilterImplOptions } from './template/filter/filter-impl-options'
import { FS } from './fs/fs'
import { toThenable, toValue } from './util/async'
import { toPromise, toValue } from './util/async'
import { Emitter } from './render/emitter'
export * from './types'
@@ -31,7 +32,7 @@ export class Liquid {
this.parser = new Parser(this)
this.renderer = new Render()
this.fs = opts.fs || fs
this.filters = new FilterMap(this.options.strictFilters)
this.filters = new FilterMap(this.options.strictFilters, this)
this.tags = new TagMap()
forOwn(builtinTags, (conf: TagImplOptions, name: string) => this.registerTag(snakeCase(name), conf))
@@ -43,26 +44,27 @@ export class Liquid {
return this.parser.parse(tokens)
}
public _render (tpl: Template[], scope?: object, opts?: LiquidOptions, sync?: boolean): IterableIterator<string> {
public _render (tpl: Template[], scope?: object, opts?: LiquidOptions, sync?: boolean): IterableIterator<any> {
const options = { ...this.options, ...normalize(opts) }
const ctx = new Context(scope, options, sync)
return this.renderer.renderTemplates(tpl, ctx)
const emitter = new Emitter(options.keepOutputType)
return this.renderer.renderTemplates(tpl, ctx, emitter)
}
public async render (tpl: Template[], scope?: object, opts?: LiquidOptions): Promise<string> {
return toThenable(this._render(tpl, scope, opts, false))
public async render (tpl: Template[], scope?: object, opts?: LiquidOptions): Promise<any> {
return toPromise(this._render(tpl, scope, opts, false))
}
public renderSync (tpl: Template[], scope?: object, opts?: LiquidOptions): string {
public renderSync (tpl: Template[], scope?: object, opts?: LiquidOptions): any {
return toValue(this._render(tpl, scope, opts, true))
}
public _parseAndRender (html: string, scope?: object, opts?: LiquidOptions, sync?: boolean): IterableIterator<string> {
public _parseAndRender (html: string, scope?: object, opts?: LiquidOptions, sync?: boolean): IterableIterator<any> {
const tpl = this.parse(html)
return this._render(tpl, scope, opts, sync)
}
public async parseAndRender (html: string, scope?: object, opts?: LiquidOptions): Promise<string> {
return toThenable(this._parseAndRender(html, scope, opts, false))
public async parseAndRender (html: string, scope?: object, opts?: LiquidOptions): Promise<any> {
return toPromise(this._parseAndRender(html, scope, opts, false))
}
public parseAndRenderSync (html: string, scope?: object, opts?: LiquidOptions): string {
public parseAndRenderSync (html: string, scope?: object, opts?: LiquidOptions): any {
return toValue(this._parseAndRender(html, scope, opts, true))
}
@@ -88,7 +90,7 @@ export class Liquid {
throw this.lookupError(file, options.root)
}
public async parseFile (file: string, opts?: LiquidOptions): Promise<Template[]> {
return toThenable(this._parseFile(file, opts, false))
return toPromise(this._parseFile(file, opts, false))
}
public parseFileSync (file: string, opts?: LiquidOptions): Template[] {
return toValue(this._parseFile(file, opts, true))
@@ -104,11 +106,11 @@ export class Liquid {
}
public _evalValue (str: string, ctx: Context): IterableIterator<any> {
const value = new Value(str, this.filters)
const value = new Value(str, this.filters, this)
return value.value(ctx)
}
public async evalValue (str: string, ctx: Context): Promise<any> {
return toThenable(this._evalValue(str, ctx))
return toPromise(this._evalValue(str, ctx))
}
public evalValueSync (str: string, ctx: Context): any {
return toValue(this._evalValue(str, ctx))
+2 -2
View File
@@ -1,4 +1,4 @@
import { VARIABLE } from '../util/character'
import { IDENTIFIER } from '../util/character'
const trie = {
a: { n: { d: { end: true, needBoundary: true } } },
@@ -19,6 +19,6 @@ export function matchOperator (str: string, begin: number, end = str.length) {
if (node['end']) info = node
}
if (!info) return -1
if (info['needBoundary'] && str.charCodeAt(i) & VARIABLE) return -1
if (info['needBoundary'] && str.charCodeAt(i) & IDENTIFIER) return -1
return i
}
+1 -1
View File
@@ -29,7 +29,7 @@ export default class Parser {
return new Tag(token, remainTokens, this.liquid)
}
if (isOutputToken(token)) {
return new Output(token as OutputToken, this.liquid.filters)
return new Output(token as OutputToken, this.liquid.filters, this.liquid)
}
return new HTML(token)
} catch (e) {
+76 -27
View File
@@ -1,6 +1,6 @@
import { whiteSpaceCtrl } from './whitespace-ctrl'
import { NumberToken } from '../tokens/number-token'
import { WordToken } from '../tokens/word-token'
import { IdentifierToken } from '../tokens/identifier-token'
import { literalValues } from '../util/literal'
import { LiteralToken } from '../tokens/literal-token'
import { OperatorToken } from '../tokens/operator-token'
@@ -20,12 +20,14 @@ import { ValueToken } from '../tokens/value-token'
import { OutputToken } from '../tokens/output-token'
import { TokenizationError } from '../util/error'
import { NormalizedFullOptions, defaultOptions } from '../liquid-options'
import { TYPES, QUOTE, BLANK, VARIABLE } from '../util/character'
import { TYPES, QUOTE, BLANK, IDENTIFIER } from '../util/character'
import { matchOperator } from './match-operator'
export class Tokenizer {
p = 0
N: number
private rawBeginAt = -1
constructor (
private input: string,
private file: string = ''
@@ -70,7 +72,7 @@ export class Tokenizer {
assert(this.peek() === '|', () => `unexpected token at ${this.snapshot()}`)
this.p++
const begin = this.p
const name = this.readWord()
const name = this.readIdentifier()
if (!name.size()) return null
const args = []
this.skipBlank()
@@ -107,8 +109,9 @@ export class Tokenizer {
readTopLevelToken (options: NormalizedFullOptions): TopLevelToken {
const { tagDelimiterLeft, outputDelimiterLeft } = options
if (this.matchWord(tagDelimiterLeft)) return this.readTagToken(options)
if (this.matchWord(outputDelimiterLeft)) return this.readOutputToken(options)
if (this.rawBeginAt > -1) return this.readEndrawOrRawContent(options)
if (this.match(tagDelimiterLeft)) return this.readTagToken(options)
if (this.match(outputDelimiterLeft)) return this.readOutputToken(options)
return this.readHTMLToken(options)
}
@@ -116,8 +119,8 @@ export class Tokenizer {
const begin = this.p
while (this.p < this.N) {
const { tagDelimiterLeft, outputDelimiterLeft } = options
if (this.matchWord(tagDelimiterLeft)) break
if (this.matchWord(outputDelimiterLeft)) break
if (this.match(tagDelimiterLeft)) break
if (this.match(outputDelimiterLeft)) break
++this.p
}
return new HTMLToken(this.input, begin, this.p, this.file)
@@ -128,9 +131,11 @@ export class Tokenizer {
const { tagDelimiterRight } = options
const begin = this.p
if (this.readTo(tagDelimiterRight) === -1) {
this.mkError(`tag ${this.snapshot(begin)} not closed`, begin)
throw this.mkError(`tag ${this.snapshot(begin)} not closed`, begin)
}
return new TagToken(input, begin, this.p, options, file)
const token = new TagToken(input, begin, this.p, options, file)
if (token.name === 'raw') this.rawBeginAt = begin
return token
}
readOutputToken (options: NormalizedFullOptions): OutputToken {
@@ -138,24 +143,59 @@ export class Tokenizer {
const { outputDelimiterRight } = options
const begin = this.p
if (this.readTo(outputDelimiterRight) === -1) {
this.mkError(`output ${this.snapshot(begin)} not closed`, begin)
throw this.mkError(`output ${this.snapshot(begin)} not closed`, begin)
}
return new OutputToken(input, begin, this.p, options, file)
}
readEndrawOrRawContent (options: NormalizedFullOptions): HTMLToken | TagToken {
const { tagDelimiterLeft, tagDelimiterRight } = options
const begin = this.p
let leftPos = this.readTo(tagDelimiterLeft) - tagDelimiterLeft.length
while (this.p < this.N) {
if (this.readIdentifier().getText() !== 'endraw') {
leftPos = this.readTo(tagDelimiterLeft) - tagDelimiterLeft.length
continue
}
while (this.p <= this.N) {
if (this.rmatch(tagDelimiterRight)) {
const end = this.p
if (begin === leftPos) {
this.rawBeginAt = -1
return new TagToken(this.input, begin, end, options, this.file)
} else {
this.p = leftPos
return new HTMLToken(this.input, begin, leftPos, this.file)
}
}
if (this.rmatch(tagDelimiterLeft)) break
this.p++
}
}
throw this.mkError(`raw ${this.snapshot(this.rawBeginAt)} not closed`, begin)
}
mkError (msg: string, begin: number) {
throw new TokenizationError(msg, new WordToken(this.input, begin, this.N, this.file))
return new TokenizationError(msg, new IdentifierToken(this.input, begin, this.N, this.file))
}
snapshot (begin: number = this.p) {
return JSON.stringify(ellipsis(this.input.slice(begin), 16))
}
readWord (): WordToken { // rename to identifier
/**
* @deprecated
*/
readWord () {
console.warn('Tokenizer#readWord() will be removed, use #readIdentifier instead')
return this.readIdentifier()
}
readIdentifier (): IdentifierToken {
this.skipBlank()
const begin = this.p
while (this.peekType() & VARIABLE) ++this.p
return new WordToken(this.input, begin, this.p, this.file)
while (this.peekType() & IDENTIFIER) ++this.p
return new IdentifierToken(this.input, begin, this.p, this.file)
}
readHashes () {
@@ -171,7 +211,7 @@ export class Tokenizer {
this.skipBlank()
if (this.peek() === ',') ++this.p
const begin = this.p
const name = this.readWord()
const name = this.readIdentifier()
if (!name.size()) return
let value
@@ -198,7 +238,7 @@ export class Tokenizer {
readTo (end: string): number {
while (this.p < this.N) {
++this.p
if (this.reverseMatchWord(end)) return this.p
if (this.rmatch(end)) return this.p
}
return -1
}
@@ -207,21 +247,30 @@ export class Tokenizer {
const value = this.readQuoted() || this.readRange()
if (value) return value
const variable = this.readWord()
if (this.peek() === '[') {
this.p++
const prop = this.readQuoted()
if (!prop) return
if (this.peek() !== ']') return
this.p++
return new PropertyAccessToken(prop, [], this.p)
}
const variable = this.readIdentifier()
if (!variable.size()) return
let isNumber = variable.isNumber(true)
const props: (QuotedToken | WordToken)[] = []
const props: (QuotedToken | IdentifierToken)[] = []
while (true) {
if (this.peek() === '[') {
isNumber = false
this.p++
const prop = this.readValue() || new WordToken(this.input, this.p, this.p, this.file)
const prop = this.readValue() || new IdentifierToken(this.input, this.p, this.p, this.file)
this.readTo(']')
props.push(prop)
} else if (this.peek() === '.' && this.peek(1) !== '.') { // skip range syntax
this.p++
const prop = this.readWord()
const prop = this.readIdentifier()
if (!prop.size()) break
if (!prop.isNumber()) isNumber = false
props.push(prop)
@@ -230,7 +279,7 @@ export class Tokenizer {
if (!props.length && literalValues.hasOwnProperty(variable.content)) {
return new LiteralToken(this.input, variable.begin, variable.end, this.file)
}
if (isNumber) return new NumberToken(variable, props[0] as WordToken)
if (isNumber) return new NumberToken(variable, props[0] as IdentifierToken)
return new PropertyAccessToken(variable, props, this.p)
}
@@ -267,22 +316,22 @@ export class Tokenizer {
return new QuotedToken(this.input, begin, this.p, this.file)
}
readFileName (): WordToken {
readFileName (): IdentifierToken {
const begin = this.p
while (!(this.peekType() & BLANK) && this.peek() !== ',' && this.p < this.N) this.p++
return new WordToken(this.input, begin, this.p, this.file)
return new IdentifierToken(this.input, begin, this.p, this.file)
}
matchWord (word: string) {
match (word: string) {
for (let i = 0; i < word.length; i++) {
if (word[i] !== this.input[this.p + i]) return false
}
return true
}
reverseMatchWord (word: string) {
for (let i = 0; i < word.length; i++) {
if (word[word.length - 1 - i] !== this.input[this.p - 1 - i]) return false
rmatch (pattern: string) {
for (let i = 0; i < pattern.length; i++) {
if (pattern[pattern.length - 1 - i] !== this.input[this.p - 1 - i]) return false
}
return true
}
-1
View File
@@ -4,7 +4,6 @@ import { NormalizedFullOptions } from '../liquid-options'
import { TYPES, INLINE_BLANK, BLANK } from '../util/character'
export function whiteSpaceCtrl (tokens: Token[], options: NormalizedFullOptions) {
options = { greedy: true, ...options }
let inRaw = false
for (let i = 0; i < tokens.length; i++) {
+11 -4
View File
@@ -1,6 +1,13 @@
export function isTruthy (val: any): boolean {
return !isFalsy(val)
import { Context } from '../context/context'
export function isTruthy (val: any, ctx: Context): boolean {
return !isFalsy(val, ctx)
}
export function isFalsy (val: any): boolean {
return val === false || undefined === val || val === null
export function isFalsy (val: any, ctx: Context): boolean {
if (ctx.opts.jsTruthy) {
return !val
} else {
return val === false || undefined === val || val === null
}
}
+23 -3
View File
@@ -1,9 +1,29 @@
import { stringify, toValue } from '../util/underscore'
export class Emitter {
public html = '';
public html: any = '';
public break = false;
public continue = false;
private keepOutputType? = false;
public write (html: string) {
this.html += html
constructor (keepOutputType: boolean|undefined) {
this.keepOutputType = keepOutputType
}
public write (html: any) {
if (this.keepOutputType === true) {
html = toValue(html)
} else {
html = stringify(toValue(html))
}
// This will only preserve the type if the value is isolated.
// I.E:
// {{ my-port }} -> 42
// {{ my-host }}:{{ my-port }} -> 'host:42'
if (this.keepOutputType === true && typeof html !== 'string' && this.html === '') {
this.html = html
} else {
this.html = stringify(this.html) + stringify(html)
}
}
}
+21 -10
View File
@@ -12,24 +12,27 @@ import { Context } from '../context/context'
import { range, toValue } from '../util/underscore'
import { Tokenizer } from '../parser/tokenizer'
import { operatorImpls } from '../render/operator'
import { UndefinedVariableError, InternalUndefinedVariableError } from '../util/error'
export class Expression {
private operands: any[] = []
private postfix: IterableIterator<Token>
private postfix: Token[]
private lenient: boolean
public constructor (str: string) {
public constructor (str: string, lenient = false) {
const tokenizer = new Tokenizer(str)
this.postfix = toPostfix(tokenizer.readExpression())
this.postfix = [...toPostfix(tokenizer.readExpression())]
this.lenient = lenient
}
public evaluate (ctx: Context): any {
for (const token of this.postfix) {
if (TypeGuards.isOperatorToken(token)) {
const r = this.operands.pop()
const l = this.operands.pop()
const result = evalOperatorToken(token, l, r)
const result = evalOperatorToken(token, l, r, ctx)
this.operands.push(result)
} else {
this.operands.push(evalToken(token, ctx))
this.operands.push(evalToken(token, ctx, this.lenient && this.postfix.length === 1))
}
}
return this.operands[0]
@@ -39,12 +42,20 @@ export class Expression {
}
}
export function evalToken (token: Token | undefined, ctx: Context): any {
export function evalToken (token: Token | undefined, ctx: Context, lenient = false): any {
assert(ctx, () => 'unable to evaluate: context not defined')
if (TypeGuards.isPropertyAccessToken(token)) {
const variable = token.variable.getText()
const variable = token.getVariableAsText()
const props: string[] = token.props.map(prop => evalToken(prop, ctx))
return ctx.get([variable, ...props])
try {
return ctx.get([variable, ...props])
} catch (e) {
if (lenient && e instanceof InternalUndefinedVariableError) {
return null
} else {
throw (new UndefinedVariableError(e, token))
}
}
}
if (TypeGuards.isRangeToken(token)) return evalRangeToken(token, ctx)
if (TypeGuards.isLiteralToken(token)) return evalLiteralToken(token)
@@ -62,9 +73,9 @@ export function evalQuotedToken (token: QuotedToken) {
return parseStringLiteral(token.getText())
}
function evalOperatorToken (token: OperatorToken, lhs: any, rhs: any) {
function evalOperatorToken (token: OperatorToken, lhs: any, rhs: any, ctx: Context) {
const impl = operatorImpls[token.operator]
return impl(lhs, rhs)
return impl(lhs, rhs, ctx)
}
function evalLiteralToken (token: LiteralToken) {
+4 -3
View File
@@ -1,8 +1,9 @@
import { isComparable } from '../drop/comparable'
import { Context } from '../context/context'
import { isFunction } from '../util/underscore'
import { isTruthy } from '../render/boolean'
export const operatorImpls: {[key: string]: (lhs: any, rhs: any) => boolean} = {
export const operatorImpls: {[key: string]: (lhs: any, rhs: any, ctx: Context) => boolean} = {
'==': (l: any, r: any) => {
if (isComparable(l)) return l.equals(r)
if (isComparable(r)) return r.equals(l)
@@ -36,6 +37,6 @@ export const operatorImpls: {[key: string]: (lhs: any, rhs: any) => boolean} = {
'contains': (l: any, r: any) => {
return l && isFunction(l.indexOf) ? l.indexOf(r) > -1 : false
},
'and': (l: any, r: any) => isTruthy(l) && isTruthy(r),
'or': (l: any, r: any) => isTruthy(l) || isTruthy(r)
'and': (l: any, r: any, ctx: Context) => isTruthy(l, ctx) && isTruthy(r, ctx),
'or': (l: any, r: any, ctx: Context) => isTruthy(l, ctx) || isTruthy(r, ctx)
}
+4 -1
View File
@@ -4,7 +4,10 @@ import { Template } from '../template/template'
import { Emitter } from './emitter'
export class Render {
public * renderTemplates (templates: Template[], ctx: Context, emitter = new Emitter()): IterableIterator<string> {
public * renderTemplates (templates: Template[], ctx: Context, emitter?: Emitter): IterableIterator<any> {
if (!emitter) {
emitter = new Emitter(ctx.opts.keepOutputType)
}
for (const tpl of templates) {
try {
const html = yield tpl.render(ctx, emitter)
+2
View File
@@ -1,5 +1,7 @@
import { Context } from '../../context/context'
import { Liquid } from '../../liquid'
export interface FilterImpl {
context: Context;
liquid: Liquid;
}
+6 -2
View File
@@ -2,11 +2,15 @@ import { FilterImplOptions } from './filter-impl-options'
import { Filter } from './filter'
import { FilterArg } from '../../parser/filter-arg'
import { assert } from '../../util/assert'
import { Liquid } from '../../liquid'
export class FilterMap {
private impls: {[key: string]: FilterImplOptions} = {}
constructor (private readonly strictFilters: boolean) {}
constructor (
private readonly strictFilters: boolean,
private readonly liquid: Liquid
) {}
get (name: string) {
const impl = this.impls[name]
@@ -19,6 +23,6 @@ export class FilterMap {
}
create (name: string, args: FilterArg[]) {
return new Filter(name, this.get(name), args)
return new Filter(name, this.get(name), args, this.liquid)
}
}
+5 -2
View File
@@ -3,16 +3,19 @@ import { Context } from '../../context/context'
import { identify } from '../../util/underscore'
import { FilterImplOptions } from './filter-impl-options'
import { FilterArg, isKeyValuePair } from '../../parser/filter-arg'
import { Liquid } from '../../liquid'
export class Filter {
public name: string
public args: FilterArg[]
private impl: FilterImplOptions
private liquid: Liquid
public constructor (name: string, impl: FilterImplOptions, args: FilterArg[]) {
public constructor (name: string, impl: FilterImplOptions, args: FilterArg[], liquid: Liquid) {
this.name = name
this.impl = impl || identify
this.args = args
this.liquid = liquid
}
public * render (value: any, context: Context) {
const argv: any[] = []
@@ -20,6 +23,6 @@ export class Filter {
if (isKeyValuePair(arg)) argv.push([arg[0], yield evalToken(arg[1], context)])
else argv.push(yield evalToken(arg, context))
}
return yield this.impl.apply({ context }, [value, ...argv])
return yield this.impl.apply({ context, liquid: this.liquid }, [value, ...argv])
}
}
+4 -4
View File
@@ -1,20 +1,20 @@
import { Value } from './value'
import { FilterMap } from './filter/filter-map'
import { stringify, toValue } from '../util/underscore'
import { TemplateImpl } from '../template/template-impl'
import { Template } from '../template/template'
import { Context } from '../context/context'
import { Emitter } from '../render/emitter'
import { OutputToken } from '../tokens/output-token'
import { Liquid } from '../liquid'
export class Output extends TemplateImpl<OutputToken> implements Template {
private value: Value
public constructor (token: OutputToken, filters: FilterMap) {
public constructor (token: OutputToken, filters: FilterMap, liquid: Liquid) {
super(token)
this.value = new Value(token.content, filters)
this.value = new Value(token.content, filters, liquid)
}
public * render (ctx: Context, emitter: Emitter) {
const val = yield this.value.value(ctx)
emitter.write(stringify(toValue(val)))
emitter.write(val)
}
}
+1 -1
View File
@@ -21,7 +21,7 @@ export class Hash {
* render (ctx: Context) {
const hash = {}
for (const key of Object.keys(this.hash)) {
hash[key] = evalToken(this.hash[key], ctx)
hash[key] = yield evalToken(this.hash[key], ctx)
}
return hash
}
+1 -2
View File
@@ -1,13 +1,12 @@
import { isFunction } from '../../util/underscore'
import { Liquid } from '../../liquid'
import { TemplateImpl } from '../../template/template-impl'
import { Emitter, Hash, Context, TagImplOptions, TagToken, Template, TopLevelToken } from '../../types'
import { Emitter, Hash, Context, TagToken, Template, TopLevelToken } from '../../types'
import { TagImpl } from './tag-impl'
export class Tag extends TemplateImpl<TagToken> implements Template {
public name: string
private impl: TagImpl
private static impls: { [key: string]: TagImplOptions } = {}
public constructor (token: TagToken, tokens: TopLevelToken[], liquid: Liquid) {
super(token)
+6 -3
View File
@@ -4,6 +4,7 @@ import { FilterMap } from '../template/filter/filter-map'
import { Filter } from './filter/filter'
import { Context } from '../context/context'
import { ValueToken } from '../tokens/value-token'
import { Liquid } from '../liquid'
export class Value {
public readonly filters: Filter[] = []
@@ -12,13 +13,15 @@ export class Value {
/**
* @param str the value to be valuated, eg.: "foobar" | truncate: 3
*/
public constructor (str: string, private readonly filterMap: FilterMap) {
public constructor (str: string, private readonly filterMap: FilterMap, liquid: Liquid) {
const tokenizer = new Tokenizer(str)
this.initial = tokenizer.readValue()
this.filters = tokenizer.readFilters().map(({ name, args }) => new Filter(name, this.filterMap.get(name), args))
this.filters = tokenizer.readFilters().map(({ name, args }) => new Filter(name, this.filterMap.get(name), args, liquid))
}
public * value (ctx: Context) {
let val = yield evalToken(this.initial, ctx)
const lenient = ctx.opts.lenientIf && this.filters.length > 0 && this.filters[0].name === 'default'
let val = yield evalToken(this.initial, ctx, lenient)
for (const filter of this.filters) {
val = yield filter.render(val, ctx)
}
+2 -2
View File
@@ -1,6 +1,6 @@
import { Token } from './token'
import { ValueToken } from './value-token'
import { WordToken } from './word-token'
import { IdentifierToken } from './identifier-token'
import { TokenKind } from '../parser/token-kind'
export class HashToken extends Token {
@@ -8,7 +8,7 @@ export class HashToken extends Token {
public input: string,
public begin: number,
public end: number,
public name: WordToken,
public name: IdentifierToken,
public value?: ValueToken,
public file?: string
) {
@@ -2,8 +2,7 @@ import { Token } from './token'
import { NUMBER, TYPES, SIGN } from '../util/character'
import { TokenKind } from '../parser/token-kind'
// a word can be an identifier, a number, a keyword or a single-word-literal
export class WordToken extends Token {
export class IdentifierToken extends Token {
public content: string
constructor (
public input: string,
+3 -3
View File
@@ -1,11 +1,11 @@
import { Token } from './token'
import { WordToken } from './word-token'
import { IdentifierToken } from './identifier-token'
import { TokenKind } from '../parser/token-kind'
export class NumberToken extends Token {
constructor (
public whole: WordToken,
public decimal?: WordToken
public whole: IdentifierToken,
public decimal?: IdentifierToken
) {
super(TokenKind.Number, whole.input, whole.begin, decimal ? decimal.end : whole.end, whole.file)
}
+12 -3
View File
@@ -1,14 +1,23 @@
import { Token } from './token'
import { WordToken } from './word-token'
import { IdentifierToken } from './identifier-token'
import { QuotedToken } from './quoted-token'
import { TokenKind } from '../parser/token-kind'
import { parseStringLiteral } from '../parser/parse-string-literal'
export class PropertyAccessToken extends Token {
constructor (
public variable: WordToken,
public props: (WordToken | QuotedToken | PropertyAccessToken)[],
public variable: IdentifierToken | QuotedToken,
public props: (IdentifierToken | QuotedToken | PropertyAccessToken)[],
end: number
) {
super(TokenKind.PropertyAccess, variable.input, variable.begin, end, variable.file)
}
getVariableAsText () {
if (this.variable instanceof IdentifierToken) {
return this.variable.getText()
} else {
return parseStringLiteral(this.variable.getText())
}
}
}
+5 -7
View File
@@ -1,8 +1,8 @@
import { DelimitedToken } from './delimited-token'
import { BLANK, TYPES, VARIABLE } from '../util/character'
import { TokenizationError } from '../util/error'
import { NormalizedFullOptions } from '../liquid-options'
import { TokenKind } from '../parser/token-kind'
import { Tokenizer } from '../parser/tokenizer'
export class TagToken extends DelimitedToken {
public name: string
@@ -18,13 +18,11 @@ export class TagToken extends DelimitedToken {
const value = input.slice(begin + tagDelimiterLeft.length, end - tagDelimiterRight.length)
super(TokenKind.Tag, value, input, begin, end, trimTagLeft, trimTagRight, file)
let nameEnd = 0
while (TYPES[this.content.charCodeAt(nameEnd)] & VARIABLE) nameEnd++
this.name = this.content.slice(0, nameEnd)
const tokenizer = new Tokenizer(this.content)
this.name = tokenizer.readIdentifier().getText()
if (!this.name) throw new TokenizationError(`illegal tag syntax`, this)
let argsBegin = nameEnd
while (TYPES[this.content.charCodeAt(argsBegin)] & BLANK) argsBegin++
this.args = this.content.slice(argsBegin)
tokenizer.skipBlank()
this.args = tokenizer.remaining()
}
}
+1
View File
@@ -17,3 +17,4 @@ export { TopLevelToken } from './tokens/toplevel-token'
export { Tokenizer } from './parser/tokenizer'
export { Hash } from './template/tag/hash'
export { evalToken, evalQuotedToken } from './render/expression'
export { toPromise, toThenable, toValue } from './util/async'
+16 -10
View File
@@ -7,7 +7,7 @@ interface Thenable {
catch (reject: resolver): Thenable;
}
function mkResolve (value: any) {
function createResolvedThenable (value: any): Thenable {
const ret = {
then: (resolve: resolver) => resolve(value),
catch: () => ret
@@ -15,7 +15,7 @@ function mkResolve (value: any) {
return ret
}
function mkReject (err: Error) {
function createRejectedThenable (err: Error): Thenable {
const ret = {
then: (resolve: resolver, reject?: resolver) => {
if (reject) return reject(err)
@@ -30,43 +30,49 @@ function isThenable (val: any): val is Thenable {
return val && isFunction(val.then)
}
function isCustomIterable (val: any): val is IterableIterator<any> {
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 (val: IterableIterator<any> | Thenable | any): Thenable {
if (isThenable(val)) return val
if (isCustomIterable(val)) return reduce()
return mkResolve(val)
if (isAsyncIterator(val)) return reduce()
return createResolvedThenable(val)
function reduce (prev?: any): Thenable {
let state
try {
state = (val as IterableIterator<any>).next(prev)
} catch (err) {
return mkReject(err)
return createRejectedThenable(err)
}
if (state.done) return mkResolve(state.value)
if (state.done) return createResolvedThenable(state.value)
return toThenable(state.value!).then(reduce, err => {
let state
try {
state = (val as IterableIterator<any>).throw!(err)
} catch (e) {
return mkReject(e)
return createRejectedThenable(e)
}
if (state.done) return mkResolve(state.value)
if (state.done) return createResolvedThenable(state.value)
return reduce(state.value)
})
}
}
export function toPromise (val: IterableIterator<any> | Thenable | any): Promise<any> {
return Promise.resolve(toThenable(val))
}
// get the value of async iterator in synchronous manner
export function toValue (val: IterableIterator<any> | Thenable | any) {
let ret: any
toThenable(val)
.then((x: any) => {
ret = x
return mkResolve(ret)
return createResolvedThenable(ret)
})
.catch((err: Error) => {
throw err
+1 -1
View File
@@ -3,7 +3,7 @@
// This file is generated by bin/character-gen.js
// bitmask character types to boost performance
export const TYPES = [0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 4, 4, 4, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 2, 8, 0, 0, 0, 0, 8, 0, 0, 0, 64, 0, 65, 0, 0, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 0, 0, 2, 2, 2, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0]
export const VARIABLE = 1
export const IDENTIFIER = 1
export const OPERATOR = 2
export const BLANK = 4
export const QUOTE = 8
+21
View File
@@ -48,6 +48,27 @@ export class RenderError extends LiquidError {
}
}
export class UndefinedVariableError extends LiquidError {
public constructor (err: Error, token: Token) {
super(err, token)
this.name = 'UndefinedVariableError'
this.message = err.message
super.update()
}
}
// only used internally; raised where we don't have token information,
// so it can't be an UndefinedVariableError.
export class InternalUndefinedVariableError extends Error {
variableName: string
public constructor (variableName: string) {
super(`undefined variable: ${variableName}`)
this.name = 'InternalUndefinedVariableError'
this.variableName = variableName
}
}
export class AssertionError extends Error {
public constructor (message: string) {
super(message)
+28 -1
View File
@@ -140,7 +140,12 @@ const formatCodes = {
};
(formatCodes as any).h = formatCodes.b
export default function (d: Date, formatStr: string) {
export default function (inputDate: Date, formatStr: string) {
let d = inputDate
if (d instanceof TimezoneDate) {
d = d.getDisplayDate()
}
let output = ''
let remaining = formatStr
let match
@@ -168,3 +173,25 @@ function format (d: Date, match: RegExpExecArray) {
if (flags['-']) padWidth = 0
return padStart(ret, padWidth, padChar)
}
export class TimezoneDate extends Date {
ISO8601_TIMEZONE_PATTERN = /([zZ]|([+-])(\d{2}):(\d{2}))$/;
inputTimezoneOffset = 0;
constructor (public dateString: string) {
super(dateString)
const m = dateString.match(this.ISO8601_TIMEZONE_PATTERN)
if (m && m[1] === 'Z') {
this.inputTimezoneOffset = this.getTimezoneOffset()
} else if (m && m[2] && m[3] && m[4]) {
const [, , sign, hours, minutes] = m
const delta = (sign === '+' ? 1 : -1) * (parseInt(hours, 10) * 60 + parseInt(minutes, 10))
this.inputTimezoneOffset = this.getTimezoneOffset() + delta
}
}
getDisplayDate (): Date {
return new Date((+this) + this.inputTimezoneOffset * 60 * 1000)
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
import { OperatorToken } from '../tokens/operator-token'
import { DelimitedToken } from '../tokens/delimited-token'
import { WordToken } from '../tokens/word-token'
import { IdentifierToken } from '../tokens/identifier-token'
import { TagToken } from '../tokens/tag-token'
import { HTMLToken } from '../tokens/html-token'
import { OutputToken } from '../tokens/output-token'
@@ -47,7 +47,7 @@ export function isPropertyAccessToken (val: any): val is PropertyAccessToken {
return getKind(val) === TokenKind.PropertyAccess
}
export function isWordToken (val: any): val is WordToken {
export function isWordToken (val: any): val is IdentifierToken {
return getKind(val) === TokenKind.Word
}
+2 -2
View File
@@ -5,7 +5,7 @@ describe('#evalValueSync()', function () {
var engine: Liquid
beforeEach(() => { engine = new Liquid() })
it('should throw when scope undefined', async function () {
return expect(() => engine.evalValueSync('foo', null as any)).to.throw(/context not defined/)
it('should eval value syncly', async function () {
return expect(engine.evalValueSync('true', { opts: {} } as any)).to.equal(true)
})
})
+3 -2
View File
@@ -5,7 +5,8 @@ describe('#evalValue()', function () {
var engine: Liquid
beforeEach(() => { engine = new Liquid() })
it('should throw when scope undefined', async function () {
return expect(engine.evalValue('"foo"', null as any)).to.be.rejectedWith(/context not defined/)
it('should eval value', async function () {
const val = await engine.evalValue('true', { opts: {} } as any)
expect(val).to.equal(true)
})
})
+55
View File
@@ -10,4 +10,59 @@ describe('Issues', function () {
const html = engine.parseAndRenderSync('{{huh | truncate: 11}}', { huh: 'fdsafdsafdsafdsaaaaa' })
expect(html).to.equal('fdsafdsa...')
})
it('#252 "Not valid identifier" error for a quotes-containing identifier', async () => {
const template = `{% capture "form_classes" -%}
foo
{%- endcapture %}{{form_classes}}`
const engine = new Liquid()
const html = await engine.parseAndRender(template)
expect(html).to.equal('foo')
})
it('#259 complex property access with braces is not supported', async () => {
const engine = new Liquid()
const html = engine.parseAndRenderSync('{{ ["complex key"] }}', { 'complex key': 'foo' })
expect(html).to.equal('foo')
})
it('#243 Potential for ReDoS through string replace function', async () => {
const engine = new Liquid()
const INPUT = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!'
const BROKEN_REGEX = /([a-z]+)+$/
// string filters vulnerable to regexp parameter: split, replace, replace_first, remove_first
const parameters = { input: INPUT, regex: BROKEN_REGEX }
const template = `{{ input | replace:regex,'' }}`
const html = engine.parseAndRenderSync(template, parameters)
// should stringify the regexp rather than execute it
expect(html).to.equal(INPUT)
})
it('#263 raw/endraw block not ignoring {% characters', () => {
const template = `{% raw %}This is a code snippet showing how {% breaks the raw block.{% endraw %}`
const engine = new Liquid()
const html = engine.parseAndRenderSync(template)
expect(html).to.equal('This is a code snippet showing how {% breaks the raw block.')
})
it('#268 elsif is not supported for unless', () => {
const template = `{%- unless condition1 -%}
<div>X</div>
{%- elsif condition2 -%}
<div>Y</div>
{%- else %}
<div>Z</div>
{% endunless %}`
const engine = new Liquid()
const html = engine.parseAndRenderSync(template, { condition1: true, condition2: true })
expect(html).to.equal('<div>Y</div>')
})
it('#277 Passing liquid in FilterImpl', () => {
const engine = new Liquid()
engine.registerFilter('render', function (template: string, name: string) {
return this.liquid.parseAndRenderSync(decodeURIComponent(template), { name })
})
const html = engine.parseAndRenderSync(
`{{ subtemplate | render: "foo" }}`,
{ subtemplate: encodeURIComponent('hello {{ name }}') }
)
expect(html).to.equal('hello foo')
})
})
+1 -1
View File
@@ -1,4 +1,4 @@
import { Liquid } from '../../dist/liquid.js'
import { Liquid } from '../../dist/liquid.browser.umd.js'
import * as sinon from 'sinon'
import { expect, use } from 'chai'
import { JSDOM } from 'jsdom'
+19 -2
View File
@@ -1,3 +1,4 @@
import { LiquidOptions } from '../../../../src/liquid-options'
import { test } from '../../../stub/render'
describe('filters/date', function () {
@@ -11,8 +12,24 @@ describe('filters/date', function () {
it('should create a new Date when given "today"', function () {
return test('{{ "today" | date: "%Y"}}', (new Date()).getFullYear().toString())
})
it('should parse as Date when given UTC string', function () {
return test('{{ "1991-02-22T00:00:00" | date: "%Y"}}', '1991')
it('should parse as Date when given a timezoneless string', function () {
return test('{{ "1991-02-22T00:00:00" | date: "%Y-%m-%dT%H:%M:%S"}}', '1991-02-22T00:00:00')
})
describe('when preserveTimezones is enabled', function () {
const opts: LiquidOptions = { preserveTimezones: true }
it('should not change the timezone between input and output', function () {
return test('{{ "1990-12-31T23:00:00Z" | date: "%Y-%m-%dT%H:%M:%S"}}', '1990-12-31T23:00:00', undefined, opts)
})
it('should apply numeric timezone offset (0)', function () {
return test('{{ "1990-12-31T23:00:00+00:00" | date: "%Y-%m-%dT%H:%M:%S"}}', '1990-12-31T23:00:00', undefined, opts)
})
it('should apply numeric timezone offset (-1)', function () {
return test('{{ "1990-12-31T23:00:00-01:00" | date: "%Y-%m-%dT%H:%M:%S"}}', '1990-12-31T23:00:00', undefined, opts)
})
it('should apply numeric timezone offset (+2.30)', function () {
return test('{{ "1990-12-31T23:00:00+02:30" | date: "%Y-%m-%dT%H:%M:%S"}}', '1990-12-31T23:00:00', undefined, opts)
})
})
it('should render string as string if not valid', function () {
return test('{{ "foo" | date: "%Y"}}', 'foo')
+6
View File
@@ -13,6 +13,12 @@ describe('tags/capture', function () {
return expect(html).to.equal('A')
})
it('should support quoted variable name', async function () {
const src = '{% capture "f" %}{{"a" | capitalize}}{%endcapture%}{{f}}'
const html = await liquid.parseAndRender(src)
return expect(html).to.equal('A')
})
it('should shading rather than overwriting', async function () {
const src = '{% capture var %}10{% endcapture %}{{var}}'
const ctx = { 'var': 20 }
+8
View File
@@ -20,15 +20,23 @@ describe('tags/for', function () {
nullProtoObj: Object.create(null),
obj: { foo: 'bar', coo: 'haa' },
alpha: ['a', 'b', 'c'],
promiseArray: Promise.resolve(['a', 'b', 'c']),
emptyArray: []
}
})
it('should support array', async function () {
const src = '{%for c in alpha%}{{c}}{%endfor%}'
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('abc')
})
it('should support promise of array', async function () {
const src = '{%for c in promiseArray%}{{c}}{%endfor%}'
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('abc')
})
it('should support object', async function () {
const src = '{%for item in obj%}{{item[0]}},{{item[1]}}-{%else%}b{%endfor%}'
const html = await liquid.parseAndRender(src, scope)
+5 -1
View File
@@ -1,5 +1,9 @@
import { Liquid } from '../../../../src/liquid'
import { expect } from 'chai'
import * as chai from 'chai'
import * as chaiAsPromised from 'chai-as-promised'
const expect = chai.expect
chai.use(chaiAsPromised)
describe('tags/if', function () {
const liquid = new Liquid()
+7 -3
View File
@@ -1,11 +1,15 @@
import { Liquid } from '../../../../src/liquid'
import { expect } from 'chai'
import * as chai from 'chai'
import * as chaiAsPromised from 'chai-as-promised'
const expect = chai.expect
chai.use(chaiAsPromised)
describe('tags/raw', function () {
const liquid = new Liquid()
it('should throw when not closed', async function () {
const p = liquid.parseAndRender('{% raw%}')
return expect(p).be.rejectedWith(/{% raw%} not closed/)
const p = liquid.parseAndRender('{% raw %}')
return expect(p).be.rejectedWith(/{% raw %} not closed/)
})
it('should output filters as it is', async function () {
const src = '{% raw %}{{ 5 | plus: 6 }}{% endraw %} is equal to 11.'
+10
View File
@@ -14,6 +14,16 @@ describe('tags/tablerow', function () {
return expect(html).to.equal(dst)
})
it('should support promises', async function () {
const src = '{% tablerow i in promiseNumbers %}{{ i }}{% endtablerow %}'
const ctx = {
promiseNumbers: Promise.resolve([1, 2, 3])
}
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 = {
+5
View File
@@ -14,6 +14,11 @@ describe('tags/unless', function () {
const html = await liquid.parseAndRender(src)
return expect(html).to.equal('no')
})
it('should support elsif', async function () {
const src = '{% unless true %}1{%elsif true%}2{%else%}3{%endunless%}'
const html = await liquid.parseAndRender(src)
return expect(html).to.equal('2')
})
it('should render unless when predicate yields false', async function () {
const src = '{% unless false %}yes{%else%}no{%endunless%}'
const html = await liquid.parseAndRender(src)
+40
View File
@@ -0,0 +1,40 @@
import { expect } from 'chai'
import { Liquid } from '../../../src/liquid'
describe('LiquidOptions#*keepOutputType*', function () {
it('should respect keepOutputType', async function () {
const engine = new Liquid({
keepOutputType: true
})
const context = {
'my-boolean': true,
'my-number': 42,
'my-string': 'test'
}
const booleanHtml = await engine.parseAndRender('{{my-boolean}}', context)
expect(booleanHtml).to.equal(true)
const numberHtml = await engine.parseAndRender('{{my-number}}', context)
expect(numberHtml).to.equal(42)
const html = await engine.parseAndRender('{{my-string}}', context)
expect(html).to.equal('test')
const composedHtml = await engine.parseAndRender('{{my-string}}:{{my-number}}', context)
expect(composedHtml).to.equal('test:42')
})
it('should respect keepOutputType = false as default', async function () {
const engine = new Liquid()
const context = {
'my-boolean': true,
'my-number': 42,
'my-string': 'test'
}
const booleanHtml = await engine.parseAndRender('{{my-boolean}}', context)
expect(booleanHtml).to.equal('true')
const numberHtml = await engine.parseAndRender('{{my-number}}', context)
expect(numberHtml).to.equal('42')
const html = await engine.parseAndRender('{{my-string}}', context)
expect(html).to.equal('test')
const composedHtml = await engine.parseAndRender('{{my-string}}:{{my-number}}', context)
expect(composedHtml).to.equal('test:42')
})
})
+6 -4
View File
@@ -1,4 +1,4 @@
import { Liquid, isFalsy } from '../../../src/liquid'
import { Liquid, Context, isFalsy } from '../../../src/liquid'
import * as chai from 'chai'
import { mock, restore } from '../../stub/mockfs'
import * as chaiAsPromised from 'chai-as-promised'
@@ -19,7 +19,7 @@ describe('Liquid', function () {
it('should call plugin with Liquid', async function () {
const engine = new Liquid()
engine.plugin(function () {
this.registerFilter('t', isFalsy)
this.registerFilter('t', function (v) { return isFalsy(v, this.context) })
})
const html = await engine.parseAndRender('{{false|t}}')
expect(html).to.equal('true')
@@ -99,14 +99,16 @@ describe('Liquid', function () {
describe('#evalValue', function () {
it('should eval string literal', async function () {
const engine = new Liquid()
const str = await engine.evalValue('"foo"', {} as any)
const ctx = new Context()
const str = await engine.evalValue('"foo"', ctx)
expect(str).to.equal('foo')
})
})
describe('#evalValueSync', function () {
it('should eval string literal', function () {
const engine = new Liquid()
const str = engine.evalValueSync('"foo"', {} as any)
const ctx = new Context()
const str = engine.evalValueSync('"foo"', ctx)
expect(str).to.equal('foo')
})
})
+31
View File
@@ -30,4 +30,35 @@ describe('LiquidOptions#strict*', function () {
return expect(engine.parseAndRender(html, ctx, opts)).to
.be.rejectedWith(/undefined variable: notdefined/)
})
describe('with strictVariables and lenientIf', function () {
const strictLenientOpts = {
strictVariables: true,
lenientIf: true
}
it('should not throw in `if` with a single variable', async function () {
const tpl = engine.parse('before{% if notdefined %}{{notdefined}}{% endif %}after')
const html = await engine.render(tpl, ctx, strictLenientOpts)
return expect(html).to.equal('beforeafter')
})
it('should support elsif with undefined variables', async function () {
const tpl = engine.parse('{% if notdefined1 %}a{% elsif notdefined2 %}b{% elsif defined3 %}{{defined3}}{% else %}d{% endif %}')
const html = await engine.render(tpl, { 'defined3': 'bla' }, strictLenientOpts)
return expect(html).to.equal('bla')
})
it('should not throw in `unless` with a single variable', async function () {
const tpl = engine.parse('before{% unless notdefined %}X{% else %}{{notdefined}}{% endunless %}after')
const html = await engine.render(tpl, ctx, strictLenientOpts)
return expect(html).to.equal('beforeXafter')
})
it('should still throw with an undefined variable in a compound `if` expression', function () {
const tpl = engine.parse('{% if notdefined == 15 %}a{% endif %}')
const fhtml = engine.render(tpl, ctx, strictLenientOpts)
return expect(fhtml).to.be.rejectedWith(/undefined variable: notdefined/)
})
it('should allow an undefined variable when before the `default` filter', async function () {
const tpl = engine.parse('{{notdefined | default: "a" | tolower}}')
const html = await engine.render(tpl, ctx, strictLenientOpts)
return expect(html).to.equal('a')
})
})
})
+5 -4
View File
@@ -1,16 +1,17 @@
import { Liquid } from '../../src/liquid'
import { LiquidOptions } from '../../src/liquid-options'
import { expect } from 'chai'
export const liquid = new Liquid()
export function render (src: string, ctx?: object) {
return liquid.parseAndRender(src, ctx)
export function render (src: string, ctx?: object, opts?: LiquidOptions) {
return liquid.parseAndRender(src, ctx, opts)
}
export async function test (src: string, ctx: object | string, dst?: string) {
export async function test (src: string, ctx: object | string, dst?: string, opts?: LiquidOptions) {
if (dst === undefined) {
dst = ctx as string
ctx = {}
}
return expect(await render(src, ctx as object)).to.equal(dst)
return expect(await render(src, ctx as object, opts)).to.equal(dst)
}
+43 -3
View File
@@ -1,5 +1,5 @@
import { expect } from 'chai'
import { WordToken } from '../../../src/tokens/word-token'
import { IdentifierToken } from '../../../src/tokens/identifier-token'
import { NumberToken } from '../../../src/tokens/number-token'
import { PropertyAccessToken } from '../../../src/tokens/property-access-token'
import { RangeToken } from '../../../src/tokens/range-token'
@@ -19,6 +19,10 @@ describe('Tokenize', function () {
expect(new Tokenizer('a[ b][ "c d" ]').readValueOrThrow().getText()).to.equal('a[ b][ "c d" ]')
expect(new Tokenizer('a.b[c[d.e]]').readValueOrThrow().getText()).to.equal('a.b[c[d.e]]')
})
it('should read identifier', () => {
expect(new Tokenizer('foo bar').readIdentifier()).to.haveOwnProperty('content', 'foo')
expect(new Tokenizer('foo bar').readWord()).to.haveOwnProperty('content', 'foo')
})
it('should read number value', () => {
const token: NumberToken = new Tokenizer('2.33.2').readValueOrThrow() as any
expect(token).to.be.instanceOf(NumberToken)
@@ -34,6 +38,19 @@ describe('Tokenize', function () {
it('should read property access value', () => {
expect(new Tokenizer('a[b]["c d"]').readValueOrThrow().getText()).to.equal('a[b]["c d"]')
})
it('should read quoted property access value', () => {
const value = new Tokenizer('["a prop"]').readValue()
expect(value).to.be.instanceOf(PropertyAccessToken)
expect((value as PropertyAccessToken).variable.getText()).to.equal('"a prop"')
})
it('should throw for broken quoted property access', () => {
const tokenizer = new Tokenizer('[5]')
expect(() => tokenizer.readValueOrThrow()).to.throw()
})
it('should throw for incomplete quoted property access', () => {
const tokenizer = new Tokenizer('["a prop"')
expect(() => tokenizer.readValueOrThrow()).to.throw()
})
it('should read hash', () => {
const hash1 = new Tokenizer('foo: 3').readHash()
expect(hash1!.name.content).to.equal('foo')
@@ -87,6 +104,29 @@ describe('Tokenize', function () {
expect(tag.name).to.equal('for')
expect(tag.args).to.equal('p in a[1]')
})
it('should allow unclosed tag inside {% raw %}', function () {
const html = '{%raw%} {%if%} {%else {%endraw%}'
const tokenizer = new Tokenizer(html)
const tokens = tokenizer.readTopLevelTokens()
expect(tokens.length).to.equal(3)
expect(tokens[0]).to.haveOwnProperty('name', 'raw')
expect((tokens[1] as any).getContent()).to.equal(' {%if%} {%else ')
})
it('should allow unclosed endraw tag inside {% raw %}', function () {
const html = '{%raw%} {%endraw {%raw%} {%endraw%}'
const tokenizer = new Tokenizer(html)
const tokens = tokenizer.readTopLevelTokens()
expect(tokens.length).to.equal(3)
expect(tokens[0]).to.haveOwnProperty('name', 'raw')
expect((tokens[1] as any).getContent()).to.equal(' {%endraw {%raw%} ')
})
it('should throw when {% raw %} not closed', function () {
const html = '{%raw%} {%endraw {%raw%}'
const tokenizer = new Tokenizer(html)
expect(() => tokenizer.readTopLevelTokens()).to.throw('raw "{%raw%} {%end..." not closed, line:1, col:8')
})
it('should read output token', function () {
const html = '<p>{{foo | date: "%Y-%m-%d"}}</p>'
const tokenizer = new Tokenizer(html)
@@ -246,7 +286,7 @@ describe('Tokenize', function () {
expect(token!.args[0]).to.be.instanceOf(PropertyAccessToken)
expect(pa.variable.content).to.equal('obj')
expect(pa.props).to.have.lengthOf(1)
expect(pa.props[0]).to.be.instanceOf(WordToken)
expect(pa.props[0]).to.be.instanceOf(IdentifierToken)
expect(pa.props[0].getText()).to.equal('foo')
})
it('should read a filter with obj["foo"] argument', function () {
@@ -316,7 +356,7 @@ describe('Tokenize', function () {
expect(pa.props).to.have.lengthOf(2)
const [p1, p2] = pa.props
expect(p1).to.be.instanceOf(WordToken)
expect(p1).to.be.instanceOf(IdentifierToken)
expect(p1.getText()).to.equal('')
expect(p2).to.be.instanceOf(PropertyAccessToken)
expect(p2.getText()).to.equal('b')
+69 -12
View File
@@ -1,38 +1,95 @@
import { isTruthy } from '../../../src/render/boolean'
import { isTruthy, isFalsy } from '../../../src/render/boolean'
import { Context } from '../../../src/context/context'
import { expect } from 'chai'
describe('boolean', async function () {
describe('boolean Shopify', async function () {
describe('.isTruthy()', async function () {
const ctx = {
opts: {
jsTruthy: false
}
} as unknown as Context
//
// Spec: https://shopify.github.io/liquid/basics/truthy-and-falsy/
it('true is truthy', function () {
expect(isTruthy(true)).to.be.true
expect(isTruthy(true, ctx)).to.be.true
})
it('false is falsy', function () {
expect(isTruthy(false)).to.be.false
expect(isTruthy(false, ctx)).to.be.false
})
it('null is falsy', function () {
expect(isTruthy(null)).to.be.false
expect(isTruthy(null, ctx)).to.be.false
})
it('"foo" is truthy', function () {
expect(isTruthy('foo')).to.be.true
expect(isTruthy('foo', ctx)).to.be.true
})
it('"" is truthy', function () {
expect(isTruthy('')).to.be.true
expect(isTruthy('', ctx)).to.be.true
})
it('0 is truthy', function () {
expect(isTruthy(0)).to.be.true
expect(isTruthy(0, ctx)).to.be.true
})
it('1 is truthy', function () {
expect(isTruthy(1)).to.be.true
expect(isTruthy(1, ctx)).to.be.true
})
it('1.1 is truthy', function () {
expect(isTruthy(1.1)).to.be.true
expect(isTruthy(1.1, ctx)).to.be.true
})
it('[1] is truthy', function () {
expect(isTruthy([1])).to.be.true
expect(isTruthy([1], ctx)).to.be.true
})
it('[] is truthy', function () {
expect(isTruthy([])).to.be.true
expect(isTruthy([], ctx)).to.be.true
})
})
})
describe('boolean jsTruthy', async function () {
const ctx = {
opts: {
jsTruthy: true
}
} as unknown as Context
describe('.isFalsy()', async function () {
it('null is always falsy', function () {
expect(isFalsy(null, ctx)).to.be.true
})
})
describe('.isTruthy()', async function () {
it('true is truthy', function () {
expect(isTruthy(true, ctx)).to.be.true
})
it('false is falsy', function () {
expect(isTruthy(false, ctx)).to.be.false
})
it('null is always falsy', function () {
expect(isTruthy(null, ctx)).to.be.false
})
it('null is always falsy', function () {
expect(isTruthy(null, ctx)).to.be.false
})
it('"foo" is truthy', function () {
expect(isTruthy('foo', ctx)).to.be.true
})
it('"" is falsy', function () {
expect(isTruthy('', ctx)).to.be.false
})
it('0 is falsy', function () {
expect(isTruthy(0, ctx)).to.be.false
})
it('1 is truthy', function () {
expect(isTruthy(1, ctx)).to.be.true
})
it('1.1 is truthy', function () {
expect(isTruthy(1.1, ctx)).to.be.true
})
it('[1] is truthy', function () {
expect(isTruthy([1], ctx)).to.be.true
})
it('[] is truthy', function () {
expect(isTruthy([], ctx)).to.be.true
})
})
})
+2 -1
View File
@@ -3,6 +3,7 @@ import { Context } from '../../../src/context/context'
import { HTMLToken } from '../../../src/tokens/html-token'
import { Render } from '../../../src/render/render'
import { HTML } from '../../../src/template/html'
import { Emitter } from '../../../src/render/emitter'
import { toThenable } from '../../../src/util/async'
describe('render', function () {
@@ -15,7 +16,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))
const html = await toThenable(render.renderTemplates([new HTML(token)], scope, new Emitter(scope.opts.keepOutputType)))
return expect(html).to.equal('<p>')
})
})
+10 -8
View File
@@ -5,7 +5,7 @@ import { Context } from '../../../../src/context/context'
import { toThenable } from '../../../../src/util/async'
import { NumberToken } from '../../../../src/tokens/number-token'
import { QuotedToken } from '../../../../src/tokens/quoted-token'
import { WordToken } from '../../../../src/tokens/word-token'
import { IdentifierToken } from '../../../../src/tokens/identifier-token'
import { FilterMap } from '../../../../src/template/filter/filter-map'
chai.use(sinonChai)
@@ -14,8 +14,9 @@ const expect = chai.expect
describe('filter', function () {
let ctx: Context
let filters: FilterMap
const liquid = {} as any
beforeEach(function () {
filters = new FilterMap(false)
filters = new FilterMap(false, liquid)
ctx = new Context()
})
it('should create default filter if not registered', async function () {
@@ -30,16 +31,17 @@ describe('filter', function () {
it('should call filter impl with correct arguments', async function () {
const spy = sinon.spy()
filters.set('foo', spy)
const thirty = new NumberToken(new WordToken('30', 0, 2), undefined)
const thirty = new NumberToken(new IdentifierToken('30', 0, 2), undefined)
await toThenable(filters.create('foo', [thirty]).render('foo', ctx))
expect(spy).to.have.been.calledWith('foo', 30)
})
it('should call filter impl with correct this arg', async function () {
it('should call filter impl with correct this', async function () {
const spy = sinon.spy()
filters.set('foo', spy)
const thirty = new NumberToken(new WordToken('33', 0, 2), undefined)
const thirty = new NumberToken(new IdentifierToken('33', 0, 2), undefined)
await toThenable(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())
@@ -48,13 +50,13 @@ describe('filter', function () {
it('should render filters with argument', async function () {
filters.set('add', (a, b) => a + b)
const two = new NumberToken(new WordToken('2', 0, 1), undefined)
const two = new NumberToken(new IdentifierToken('2', 0, 1), undefined)
expect(await toThenable(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 WordToken('2', 0, 1), undefined)
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')
})
@@ -73,7 +75,7 @@ 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 WordToken('2', 0, 1), undefined)
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')
})
})
+58 -5
View File
@@ -4,14 +4,16 @@ import { Context } from '../../../src/context/context'
import { Output } from '../../../src/template/output'
import { OutputToken } from '../../../src/tokens/output-token'
import { FilterMap } from '../../../src/template/filter/filter-map'
import { defaultOptions } from '../../../src/liquid-options'
const expect = chai.expect
describe('Output', function () {
const emitter: any = { write: (html: string) => (emitter.html += html), html: '' }
const liquid = {} as any
let filters: FilterMap
beforeEach(function () {
filters = new FilterMap(false)
filters = new FilterMap(false, liquid)
emitter.html = ''
})
@@ -19,26 +21,77 @@ describe('Output', function () {
const scope = new Context({
foo: { obj: { arr: ['a', 2] } }
})
const output = new Output({ content: 'foo' } as OutputToken, filters)
const output = new Output({ content: 'foo' } as OutputToken, filters, liquid)
await toThenable(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, filters)
const output = new Output({ content: 'obj' } as OutputToken, filters, liquid)
await toThenable(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, filters)
const output = new Output({ content: 'obj' } as OutputToken, filters, liquid)
await toThenable(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, filters)
const output = new Output({ content: 'obj' } as OutputToken, filters, liquid)
await toThenable(output.render(scope, emitter))
return expect(emitter.html).to.equal('FOO')
})
context('when keepOutputType is enabled', () => {
const emitter: any = {
write: (html: any) => {
if (emitter.keepOutputType && typeof html !== 'string') {
emitter.html = html
} else {
emitter.html += html as string
}
},
html: '',
keepOutputType: true
}
beforeEach(function () {
filters = new FilterMap(false, liquid)
emitter.html = ''
})
it('should respect output variable number type', async () => {
const scope = new Context({
foo: 42
}, { ...defaultOptions, keepOutputType: true })
const output = new Output({ content: 'foo' } as OutputToken, filters, liquid)
await toThenable(output.render(scope, emitter))
return expect(emitter.html).to.equal(42)
})
it('should respect output variable boolean type', async () => {
const scope = new Context({
foo: true
}, { ...defaultOptions, keepOutputType: true })
const output = new Output({ content: 'foo' } as OutputToken, filters, liquid)
await toThenable(output.render(scope, emitter))
return expect(emitter.html).to.equal(true)
})
it('should respect output variable object type', async () => {
const scope = new Context({
foo: 'test'
}, { ...defaultOptions, keepOutputType: true })
const output = new Output({ content: 'foo' } as OutputToken, filters, liquid)
await toThenable(output.render(scope, emitter))
return expect(emitter.html).to.equal('test')
})
it('should respect output variable string type', async () => {
const scope = new Context({
foo: { a: { b: 42 } }
}, { ...defaultOptions, keepOutputType: true })
const output = new Output({ content: 'foo' } as OutputToken, filters, liquid)
await toThenable(output.render(scope, emitter))
return expect(emitter.html).to.deep.equal({ a: { b: 42 } })
})
})
})
+7 -5
View File
@@ -12,15 +12,17 @@ chai.use(sinonChai)
const expect = chai.expect
describe('Value', function () {
const liquid = {} as any
describe('#constructor()', function () {
const filterMap = new FilterMap(false)
const filterMap = new FilterMap(false, liquid)
it('should parse "foo', function () {
const tpl = new Value('foo', filterMap)
const tpl = new Value('foo', filterMap, liquid)
expect(tpl.initial!.getText()).to.equal('foo')
expect(tpl.filters).to.deep.equal([])
})
it('should parse filters in value content', function () {
const f = new Value('o | foo: a: "a"', filterMap)
const f = new Value('o | foo: a: "a"', filterMap, liquid)
expect(f.filters[0].name).to.equal('foo')
expect(f.filters[0].args).to.have.lengthOf(1)
const [k, v] = f.filters[0].args[0] as any
@@ -34,10 +36,10 @@ describe('Value', function () {
it('should call chained filters correctly', async function () {
const date = sinon.stub().returns('y')
const time = sinon.spy()
const filterMap = new FilterMap(false)
const filterMap = new FilterMap(false, liquid)
filterMap.set('date', date)
filterMap.set('time', time)
const tpl = new Value('foo.bar | date: "b" | time:2', filterMap)
const tpl = new Value('foo.bar | date: "b" | time:2', filterMap, liquid)
const scope = new Context({
foo: { bar: 'bar' }
})
+21
View File
@@ -0,0 +1,21 @@
import * as chai from 'chai'
import * as sinonChai from 'sinon-chai'
import { PropertyAccessToken } from '../../../src/tokens/property-access-token'
import { QuotedToken } from '../../../src/tokens/quoted-token'
import { IdentifierToken } from '../../../src/tokens/identifier-token'
chai.use(sinonChai)
const expect = chai.expect
describe('PropertyAccessToken', function () {
describe('getVariableAsText', function () {
it('should return correct value for IdentifierToken', function () {
const token = new PropertyAccessToken(new IdentifierToken('foo', 0, 3), [], 3)
expect(token.getVariableAsText()).to.equal('foo')
})
it('should return correct value for QuotedToken', function () {
const token = new PropertyAccessToken(new QuotedToken('"foo bar"', 0, 9), [], 9)
expect(token.getVariableAsText()).to.equal('foo bar')
})
})
})
+10 -1
View File
@@ -1,10 +1,19 @@
import { toThenable, toValue } from '../../../src/util/async'
import { toThenable, toPromise, toValue } from '../../../src/util/async'
import { expect, use } from 'chai'
import * as chaiAsPromised from 'chai-as-promised'
use(chaiAsPromised)
describe('utils/async', () => {
describe('#toPromise()', function () {
it('should return a promise', async () => {
function * foo () {
return 'foo'
}
const result = await toPromise(foo())
expect(result).to.equal('foo')
})
})
describe('#toThenable()', function () {
it('should support iterable with single return statement', async () => {
function * foo () {
+5 -5
View File
@@ -25,18 +25,18 @@ describe('util/strftime', function () {
expect(t(then, '%j')).to.equal('066')
})
it('should take count of leap years', function () {
const date = new Date('2001-03-01')
const date = new Date('2001 03 01')
expect(t(date, '%j')).to.equal('060')
})
it('should take count of leap years', function () {
const date = new Date('2000-03-01')
const date = new Date('2000 03 01')
expect(t(date, '%j')).to.equal('061')
})
})
it('should format %q as date suffix', function () {
const st = new Date('2016-03-01T03:05:03.000Z')
const nd = new Date('2016-03-02T03:05:03.000Z')
const rd = new Date('2016-03-03T03:05:03.000Z')
const st = new Date('2016-03-01 03:05:03')
const nd = new Date('2016-03-02 03:05:03')
const rd = new Date('2016-03-03 03:05:03')
expect(t(st, '%q')).to.equal('st')
expect(t(nd, '%q')).to.equal('nd')
expect(t(rd, '%q')).to.equal('rd')