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