mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-14 20:00:39 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
570400ebcc | ||
|
|
3f5e23c42b | ||
|
|
96c061bee0 | ||
|
|
edb2cc1f37 | ||
|
|
2ae8c37a4f | ||
|
|
0bc5908f17 | ||
|
|
1ffba2bc76 | ||
|
|
bbebb5515c | ||
|
|
b14430a161 | ||
|
|
505c408a85 | ||
|
|
224679a7d7 | ||
|
|
2cb4bffd3e | ||
|
|
c940e6c997 | ||
|
|
d8ba009773 | ||
|
|
fdf0043b9c | ||
|
|
bef290923c | ||
|
|
4282accaa2 | ||
|
|
6075c0a54f | ||
|
|
87489f6098 | ||
|
|
6c5dc336e7 | ||
|
|
00bc1efe6a | ||
|
|
6fb4796202 | ||
|
|
7483613b23 | ||
|
|
7762dd9612 | ||
|
|
daa0b574fc | ||
|
|
a084183246 | ||
|
|
eadb6f3711 | ||
|
|
dba26f24e6 | ||
|
|
722da6a7ee | ||
|
|
82d7673554 | ||
|
|
64dd057552 | ||
|
|
45e3c2bb8e | ||
|
|
76345a64c3 | ||
|
|
9e69652655 | ||
|
|
defbb58e66 | ||
|
|
b7b92c63cf | ||
|
|
265534b7b7 | ||
|
|
84ea2c7488 | ||
|
|
64e0c876e9 | ||
|
|
4a8088d4e4 | ||
|
|
ad0930f152 | ||
|
|
998832c772 | ||
|
|
4ba224043b | ||
|
|
5ffc904f80 | ||
|
|
08646f75b0 | ||
|
|
3647305dcf | ||
|
|
cca0306f5b | ||
|
|
eeeacc4492 | ||
|
|
d44e1d727a | ||
|
|
f960904df4 | ||
|
|
b4acdb463a | ||
|
|
21d674e22b | ||
|
|
7bee9fc92d | ||
|
|
b69c3a3203 | ||
|
|
d5b9916b2f | ||
|
|
d0118402e4 | ||
|
|
31bc71d472 | ||
|
|
53a835ab54 | ||
|
|
75b3e15ca9 | ||
|
|
f34573ee21 | ||
|
|
875f71d5f2 | ||
|
|
407ac6d0a4 | ||
|
|
f9f35ebd6c | ||
|
|
88c9e96392 | ||
|
|
54b2adce12 | ||
|
|
542968e133 | ||
|
|
3b7351d9a0 | ||
|
|
e977669118 | ||
|
|
279458c670 | ||
|
|
37d12a14eb | ||
|
|
3cf01603e8 | ||
|
|
56c7992b76 | ||
|
|
5b6100d12b | ||
|
|
51f7e66f60 | ||
|
|
952ab6e73b | ||
|
|
4171b2e023 | ||
|
|
d20a043fbb | ||
|
|
3dbab238ae | ||
|
|
d7d00421e2 | ||
|
|
c33d8f6828 | ||
|
|
c13a16fdd6 | ||
|
|
4bd35de867 | ||
|
|
29f53304bc | ||
|
|
cbebb00ef1 | ||
|
|
64a9eb79c5 | ||
|
|
852c6ad453 | ||
|
|
f432aade6a | ||
|
|
aa49b4f117 | ||
|
|
fc9ebdb90f | ||
|
|
7ee87008c7 | ||
|
|
677e8511e6 | ||
|
|
b51b0dabca | ||
|
|
1cc7249a71 | ||
|
|
a626de632c | ||
|
|
83a8e2d310 | ||
|
|
c37b330751 | ||
|
|
c8de8bcde2 | ||
|
|
2015f683b8 | ||
|
|
d14f18520b | ||
|
|
c0524a83f3 | ||
|
|
dde7b3b389 | ||
|
|
84bbb3432f | ||
|
|
1c5b934250 | ||
|
|
a2caeb559b | ||
|
|
81e6861b73 | ||
|
|
9038d56ce7 | ||
|
|
bad72005fe | ||
|
|
4dffb27e95 | ||
|
|
f38ea63c14 | ||
|
|
57bdf17dea | ||
|
|
0040485188 | ||
|
|
42d799055a | ||
|
|
8a881ac69a | ||
|
|
7a2e036c79 | ||
|
|
a31e0c6275 | ||
|
|
7df4d5d62c | ||
|
|
22b7e55fc9 | ||
|
|
0238cf2c44 | ||
|
|
601f2e48fc |
+1
-1
@@ -1,4 +1,4 @@
|
||||
node_modules
|
||||
dist
|
||||
build
|
||||
demo
|
||||
coverage
|
||||
|
||||
+8
-1
@@ -1,16 +1,23 @@
|
||||
{
|
||||
"extends": "standard",
|
||||
"env": {
|
||||
"mocha": true,
|
||||
"es6": true,
|
||||
"browser": true,
|
||||
"node": true
|
||||
},
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [
|
||||
"mocha",
|
||||
"standard",
|
||||
"@typescript-eslint",
|
||||
"promise"
|
||||
],
|
||||
"rules": {
|
||||
"no-var": 2,
|
||||
"prefer-const": 2
|
||||
"prefer-const": 2,
|
||||
"no-unused-vars": "off",
|
||||
"import/export": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["error", { "vars": "all", "args": "none", "ignoreRestSiblings": false }]
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -2,13 +2,15 @@
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# tests
|
||||
# cache
|
||||
.rpt2_cache
|
||||
coverage/
|
||||
.nyc_output/
|
||||
|
||||
# modules
|
||||
node_modules/
|
||||
build/
|
||||
dist/
|
||||
|
||||
# editors
|
||||
.*.swp
|
||||
.vscode
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
/coverage
|
||||
/test
|
||||
.*
|
||||
+17
-10
@@ -1,12 +1,19 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "8"
|
||||
- "6"
|
||||
- "4"
|
||||
before_script:
|
||||
- npm install -g mocha
|
||||
script:
|
||||
- if (( $TRAVIS_NODE_VERSION >= 6 )); then npm run test; fi
|
||||
- npm run e2e
|
||||
after_success:
|
||||
- if (( $TRAVIS_NODE_VERSION >= 6 )); then npm run coveralls; fi
|
||||
- "10"
|
||||
- "8"
|
||||
- "6"
|
||||
jobs:
|
||||
include:
|
||||
- stage: test
|
||||
name: 'Linting'
|
||||
script: npm run lint
|
||||
- name: 'Coverage'
|
||||
script: npm run coverage-coveralls
|
||||
- stage: release
|
||||
if: branch = master
|
||||
script: skip
|
||||
deploy:
|
||||
provider: script
|
||||
skip_cleanup: true
|
||||
script: npx semantic-release
|
||||
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
# [8.3.0](https://github.com/harttle/liquidjs/compare/v8.2.4...v8.3.0) (2019-06-27)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* fs option implemented by [#138](https://github.com/harttle/liquidjs/issues/138) ([3f5e23c](https://github.com/harttle/liquidjs/commit/3f5e23c))
|
||||
|
||||
## [8.2.4](https://github.com/harttle/liquidjs/compare/v8.2.3...v8.2.4) (2019-06-17)
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* improve getTemplate() when cache is enabled ([1ffba2b](https://github.com/harttle/liquidjs/commit/1ffba2b))
|
||||
|
||||
## [8.2.3](https://github.com/harttle/liquidjs/compare/v8.2.2...v8.2.3) (2019-05-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* reverse filter not pure, see [#126](https://github.com/harttle/liquidjs/issues/126) ([505c408](https://github.com/harttle/liquidjs/commit/505c408))
|
||||
|
||||
## [8.2.2](https://github.com/harttle/liquidjs/compare/v8.2.1...v8.2.2) (2019-05-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* date from integer, [#125](https://github.com/harttle/liquidjs/issues/125) ([fdf0043](https://github.com/harttle/liquidjs/commit/fdf0043))
|
||||
* pass drops directly to filters/tags ([bef2909](https://github.com/harttle/liquidjs/commit/bef2909))
|
||||
|
||||
## [8.2.1](https://github.com/harttle/liquidjs/compare/v8.2.0...v8.2.1) (2019-04-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* `default` filter is not working with an empty string, [#122](https://github.com/harttle/liquidjs/issues/122) ([6075c0a](https://github.com/harttle/liquidjs/commit/6075c0a))
|
||||
|
||||
# [8.2.0](https://github.com/harttle/liquidjs/compare/v8.1.0...v8.2.0) (2019-04-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* pass context to filters ([00bc1ef](https://github.com/harttle/liquidjs/commit/00bc1ef))
|
||||
|
||||
# [8.1.0](https://github.com/harttle/liquidjs/compare/v8.0.3...v8.1.0) (2019-04-02)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* where filter, working on [#118](https://github.com/harttle/liquidjs/issues/118) ([daa0b57](https://github.com/harttle/liquidjs/commit/daa0b57))
|
||||
|
||||
## [8.0.3](https://github.com/harttle/liquidjs/compare/v8.0.2...v8.0.3) (2019-04-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* slice filter on negative `begin`, [#117](https://github.com/harttle/liquidjs/issues/117) ([eadb6f3](https://github.com/harttle/liquidjs/commit/eadb6f3))
|
||||
|
||||
## [8.0.2](https://github.com/harttle/liquidjs/compare/v8.0.1...v8.0.2) (2019-03-25)
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* use polymophism instead duck test ([82d7673](https://github.com/harttle/liquidjs/commit/82d7673))
|
||||
|
||||
## [8.0.1](https://github.com/harttle/liquidjs/compare/v8.0.0...v8.0.1) (2019-03-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* incorrect scope when using assign with for, fixes [#115](https://github.com/harttle/liquidjs/issues/115) ([defbb58](https://github.com/harttle/liquidjs/commit/defbb58))
|
||||
|
||||
# [8.0.0](https://github.com/harttle/liquidjs/compare/v7.5.1...v8.0.0) (2019-03-10)
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* use camelCase for JavaScript APIs ([64e0c87](https://github.com/harttle/liquidjs/commit/64e0c87))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* promise support for drops, working on [#65](https://github.com/harttle/liquidjs/issues/65) ([4a8088d](https://github.com/harttle/liquidjs/commit/4a8088d))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* Options and method names in JavaScript API are now renamed to cammelCase, for a complete list see #109
|
||||
|
||||
## [7.5.1](https://github.com/harttle/liquidjs/compare/v7.5.0...v7.5.1) (2019-03-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* named params for filters, working on [#113](https://github.com/harttle/liquidjs/issues/113) ([5ffc904](https://github.com/harttle/liquidjs/commit/5ffc904))
|
||||
|
||||
# [7.5.0](https://github.com/harttle/liquidjs/compare/v7.4.0...v7.5.0) (2019-03-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* tablerowloop object ([3647305](https://github.com/harttle/liquidjs/commit/3647305))
|
||||
|
||||
# [7.4.0](https://github.com/harttle/liquidjs/compare/v7.3.1...v7.4.0) (2019-02-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* math filters now return number, resolves [#110](https://github.com/harttle/liquidjs/issues/110) ([b4acdb4](https://github.com/harttle/liquidjs/commit/b4acdb4))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* exported Drop interface for [#107](https://github.com/harttle/liquidjs/issues/107) ([7bee9fc](https://github.com/harttle/liquidjs/commit/7bee9fc)), closes [#109](https://github.com/harttle/liquidjs/issues/109)
|
||||
|
||||
## [7.3.1](https://github.com/harttle/liquidjs/compare/v7.3.0...v7.3.1) (2019-02-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **#108:** remove absolute path in emitted d.ts ([53a835a](https://github.com/harttle/liquidjs/commit/53a835a)), closes [#108](https://github.com/harttle/liquidjs/issues/108)
|
||||
|
||||
# [7.3.0](https://github.com/harttle/liquidjs/compare/v7.2.2...v7.3.0) (2019-02-24)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* nil/null/empty/blank literals, resolves [#102](https://github.com/harttle/liquidjs/issues/102) ([88c9e96](https://github.com/harttle/liquidjs/commit/88c9e96))
|
||||
|
||||
## [7.2.2](https://github.com/harttle/liquidjs/compare/v7.2.1...v7.2.2) (2019-02-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* filters break when argument contains [()|, fixes [#89](https://github.com/harttle/liquidjs/issues/89) ([e977669](https://github.com/harttle/liquidjs/commit/e977669))
|
||||
|
||||
## [7.2.1](https://github.com/harttle/liquidjs/compare/v7.2.0...v7.2.1) (2019-02-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* default length for truncate and truncatewords ([56c7992](https://github.com/harttle/liquidjs/commit/56c7992))
|
||||
|
||||
# [7.2.0](https://github.com/harttle/liquidjs/compare/v7.1.0...v7.2.0) (2019-02-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* override output/tag delimiter, fixes [#54](https://github.com/harttle/liquidjs/issues/54) ([d20a043](https://github.com/harttle/liquidjs/commit/d20a043))
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* `trim_value_left` option renamed to `trim_output_left`, `trim_value_right` option renamed to `trim_output_right`
|
||||
|
||||
# [7.1.0](https://github.com/harttle/liquidjs/compare/v7.0.2...v7.1.0) (2019-02-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* throw an Error if delimiter not matched ([c33d8f6](https://github.com/harttle/liquidjs/commit/c33d8f6))
|
||||
|
||||
# [7.0.0](https://github.com/harttle/liquidjs/compare/v6.4.3...v7.0.0) (2019-02-14)
|
||||
|
||||
|
||||
### chore
|
||||
|
||||
* **TypeScript:** ship Liquid to class ([1cc7249](https://github.com/harttle/liquidjs/commit/1cc7249))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* **TypeScript:** calling `Liquid()` without `new` now becomes invalid
|
||||
|
||||
## [6.4.3](https://github.com/harttle/liquidjs/compare/v6.4.2...v6.4.3) (2019-02-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* better index.d.ts and a demo ([2015f68](https://github.com/harttle/liquidjs/commit/2015f68)), closes [#98](https://github.com/harttle/liquidjs/issues/98)
|
||||
|
||||
## [6.4.2](https://github.com/harttle/liquidjs/compare/v6.4.1...v6.4.2) (2019-01-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **CI:** e2e not building cjs ([dde7b3b](https://github.com/harttle/liquidjs/commit/dde7b3b))
|
||||
|
||||
## [6.4.1](https://github.com/harttle/liquidjs/compare/v6.4.0...v6.4.1) (2019-01-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* regenerator undefined ([a2caeb5](https://github.com/harttle/liquidjs/commit/a2caeb5))
|
||||
@@ -9,86 +9,81 @@
|
||||
[](https://david-dm.org/harttle/liquidjs)
|
||||
[](https://david-dm.org/harttle/liquidjs?type=dev)
|
||||
[](https://github.com/harttle/liquidjs/blob/master/LICENSE)
|
||||
[](http://github.com/harttle/liquidjs)
|
||||
[](https://github.com/harttle/liquidjs)
|
||||
|
||||
This is a liquid implementation for both Node.js and browsers. Website: <http://harttle.github.io/liquidjs/>, Live Demo: <https://jsfiddle.net/6u40xbzs/>
|
||||
A [shopify][shopify/liquid] compatible [Liquid][tutorial] template engine in pure JavaScript.
|
||||
|
||||
**Features**
|
||||
Install via npm:
|
||||
|
||||
* Fully compatible to [shopify][shopify/liquid], with all [tags][tags] and [filters][filters] implemented
|
||||
* Support layout(extend) and include syntax
|
||||
* In pure JavaScript with Promise-based API
|
||||
```bash
|
||||
npm install --save liquidjs
|
||||
```
|
||||
|
||||
**Differences**
|
||||
Or include the UMD build:
|
||||
|
||||
Though being compatible with [Ruby Liquid](https://github.com/shopify/liquid) is one of our priorities, there're still certain differences. You may need some configuration to get it compatible in these senarios:
|
||||
```html
|
||||
<script src="//unpkg.com/liquidjs/dist/liquid.min.js"></script> <!--for production-->
|
||||
<script src="//unpkg.com/liquidjs/dist/liquid.js"></script> <!--for development-->
|
||||
```
|
||||
|
||||
* Dynamic file locating (enabled by default), which means layout/partial name can be an variable in liquidjs. See [#51](https://github.com/harttle/liquidjs/issues/51).
|
||||
* Truthy and Falsy. All values except `undefined`, `null`, `false` are truthy, whereas in Ruby Liquid all except `nil` and `false` are truthy. See [#26](https://github.com/harttle/liquidjs/pull/26).
|
||||
* Number Rendering. Since JavaScript do not distinguish `float` and `integer`, we cannot either convert between them nor render regarding to their type. See [#59](https://github.com/harttle/liquidjs/issues/59).
|
||||
* Along with [.to_liquid()](https://github.com/Shopify/liquid/wiki/Introduction-to-Drops), we provide an alias `.toLiquid()` to align with your code styles.
|
||||
You may need a [Promise polyfill][pp] for Node.js < 4 and ES5 browsers like [IE and Android UC][caniuse-promises].
|
||||
|
||||
## TOC
|
||||
|
||||
* Usage
|
||||
* Get Started
|
||||
* [Render from String](#render-from-string)
|
||||
* [Render from File](#render-from-file)
|
||||
* [Use with Express.js](#use-with-expressjs)
|
||||
* [Use in Browser](#use-in-browser)
|
||||
* [Include Partials](#include-partials)
|
||||
* [Layout Templates (Extends)](#layout-templates-extends)
|
||||
* API Spec
|
||||
* [Constructor Options](#options)
|
||||
* Demos
|
||||
* Node.js: [/demo/node/](demo/node/)
|
||||
* Browser: <https://jsfiddle.net/6u40xbzs/>, [/demo/browser/](demo/browser/).
|
||||
* Express.js: [/demo/express/](demo/express/)
|
||||
* TypeScript: [/demo/typescript/](demo/typescript/)
|
||||
* React JS: [/demo/reactjs/](demo/reactjs/)
|
||||
* Advanced
|
||||
* [Options](#options)
|
||||
* [Register Filters](#register-filters), [Builtin Filters](https://github.com/harttle/liquidjs/wiki/Builtin-Filters)
|
||||
* [Register Tags](#register-tags), [Builtin Tags](https://github.com/harttle/liquidjs/wiki/Builtin-Tags)
|
||||
* [Operators](https://github.com/harttle/liquidjs/wiki/Operators)
|
||||
* [Whitespace Control](https://github.com/harttle/liquidjs/wiki/Whitespace-Control)
|
||||
* [Plugin API](#plugin-api)
|
||||
* [Differences With shopify/liquid](#differences-with-shopify%2fliquid)
|
||||
* [Contribute Guidelines](#contribute-guidelines)
|
||||
|
||||
## Render from String
|
||||
|
||||
Install as Node.js dependency:
|
||||
|
||||
```bash
|
||||
# You'll need a promise-polyfill for Node.js < 4
|
||||
npm install --save liquidjs
|
||||
```
|
||||
|
||||
Parse and Render:
|
||||
Just render a template string with a context:
|
||||
|
||||
```javascript
|
||||
var Liquid = require('liquidjs');
|
||||
var engine = Liquid();
|
||||
var engine = new Liquid();
|
||||
|
||||
engine
|
||||
.parseAndRender('{{name | capitalize}}', {name: 'alice'})
|
||||
.then(console.log);
|
||||
|
||||
// outputs 'Alice'
|
||||
.then(console.log); // outputs 'Alice'
|
||||
```
|
||||
|
||||
Caching templates:
|
||||
Caching parsed templates:
|
||||
|
||||
```javascript
|
||||
var tpl = engine.parse('{{name | capitalize}}');
|
||||
engine
|
||||
.render(tpl, {name: 'alice'})
|
||||
.then(console.log);
|
||||
|
||||
// outputs 'Alice'
|
||||
.then(console.log); // outputs 'Alice'
|
||||
```
|
||||
|
||||
## Render from File
|
||||
|
||||
```javascript
|
||||
var engine = Liquid({
|
||||
root: path.resolve(__dirname, 'views/'), // dirs to lookup layouts/includes
|
||||
extname: '.liquid' // the extname used for layouts/includes, defaults ""
|
||||
var engine = new Liquid({
|
||||
root: path.resolve(__dirname, 'views/'), // root for layouts/includes lookup
|
||||
extname: '.liquid' // used for layouts/includes, defaults ""
|
||||
});
|
||||
engine.renderFile("hello.liquid", {name: 'alice'})
|
||||
.then(console.log) // outputs "Alice"
|
||||
|
||||
// which is equivalent to:
|
||||
engine
|
||||
.renderFile("hello", {name: 'alice'})
|
||||
.renderFile("hello", {name: 'alice'}) // will read and render `views/hello.liquid`
|
||||
.then(console.log) // outputs "Alice"
|
||||
```
|
||||
|
||||
@@ -101,24 +96,9 @@ app.set('views', './views'); // specify the views directory
|
||||
app.set('view engine', 'liquid'); // set to default
|
||||
```
|
||||
|
||||
[Here](demo/express/)'s an Express demo. When used with Express.js,
|
||||
Express [`views`][express-views] will be included when looking up
|
||||
[`views`][express-views] in express.js will be included when looking up
|
||||
partials(includes and layouts).
|
||||
|
||||
## Use in Browser
|
||||
|
||||
You can get a dist file for browsers from
|
||||
|
||||
* [Releases][releases] page for liquidjs, or
|
||||
* unpkg.com: <https://unpkg.com/liquidjs/dist/liquid.min.js>
|
||||
|
||||
Here's the demo:
|
||||
|
||||
* JSFiddle: <https://jsfiddle.net/6u40xbzs/>
|
||||
* Demo directory: [/demo/browser/](demo/browser/).
|
||||
|
||||
Note: For [IE and Android UC][caniuse-promises] browser, you will need a [Promise polyfill][pp].
|
||||
|
||||
## Include Partials
|
||||
|
||||
```
|
||||
@@ -178,21 +158,27 @@ Defaults to `["."]`
|
||||
|
||||
* `dynamicPartials`: if set, treat `<filepath>` parameter in `{%include filepath %}`, `{%layout filepath%}` as a variable, otherwise as a literal value. Defaults to `true`.
|
||||
|
||||
* `strict_filters` is used to enable strict filter existence. If set to `false`, undefined filters will be rendered as empty string. Otherwise, undefined filters will cause an exception. Defaults to `false`.
|
||||
* `strictFilters` is used to enable strict filter existence. If set to `false`, undefined filters will be rendered as empty string. Otherwise, undefined filters will cause an exception. Defaults to `false`.
|
||||
|
||||
* `strict_variables` is used to enable strict variable derivation.
|
||||
* `strictVariables` is used to enable strict variable derivation.
|
||||
If set to `false`, undefined variables will be rendered as empty string.
|
||||
Otherwise, undefined variables will cause an exception. Defaults to `false`.
|
||||
|
||||
* `trim_tag_right` is used to strip blank characters (including ` `, `\t`, and `\r`) from the right of tags (`{% %}`) until `\n` (inclusive). Defaults to `false`.
|
||||
* `trimTagRight` is used to strip blank characters (including ` `, `\t`, and `\r`) from the right of tags (`{% %}`) until `\n` (inclusive). Defaults to `false`.
|
||||
|
||||
* `trim_tag_left` is similiar to `trim_tag_right`, whereas the `\n` is exclusive. Defaults to `false`. See [Whitespace Control][whitespace control] for details.
|
||||
* `trimTagLeft` is similiar to `trimTagRight`, whereas the `\n` is exclusive. Defaults to `false`. See [Whitespace Control][whitespace control] for details.
|
||||
|
||||
* `trim_value_right` is used to strip blank characters (including ` `, `\t`, and `\r`) from the right of values (`{{ }}`) until `\n` (inclusive). Defaults to `false`.
|
||||
* `trimOutputRight` is used to strip blank characters (including ` `, `\t`, and `\r`) from the right of values (`{{ }}`) until `\n` (inclusive). Defaults to `false`.
|
||||
|
||||
* `trim_value_left` is similiar to `trim_value_right`, whereas the `\n` is exclusive. Defaults to `false`. See [Whitespace Control][whitespace control] for details.
|
||||
* `trimOutputLeft` is similiar to `trimOutputRight`, whereas the `\n` is exclusive. Defaults to `false`. See [Whitespace Control][whitespace control] for details.
|
||||
|
||||
* `greedy` is used to specify whether `trim_left`/`trim_right` is greedy. When set to `true`, all consecutive blank characters including `\n` will be trimed regardless of line breaks. Defaults to `true`.
|
||||
* `tagDelimiterLeft` and `tagDelimiterRight` are used to override the delimiter for liquid tags.
|
||||
|
||||
* `outputDelimiterLeft` and `outputDelimiterRight` are used to override the delimiter for liquid outputs.
|
||||
|
||||
* `greedy` is used to specify whether `trim*Left`/`trim*Right` is greedy. When set to `true`, all consecutive blank characters including `\n` will be trimed regardless of line breaks. Defaults to `true`.
|
||||
|
||||
* `fs` is used to override the default file-system module with a custom implementation.
|
||||
|
||||
## Register Filters
|
||||
|
||||
@@ -208,7 +194,7 @@ Filter arguments will be passed to the registered filter function, for example:
|
||||
engine.registerFilter('add', (initial, arg1, arg2) => initial + arg1 + arg2)
|
||||
```
|
||||
|
||||
See existing filter implementations here: <https://github.com/harttle/liquidjs/blob/master/filters.js>
|
||||
See existing filter implementations here: <https://github.com/harttle/liquidjs/tree/master/src/builtin/filters>
|
||||
|
||||
## Register Tags
|
||||
|
||||
@@ -218,9 +204,9 @@ engine.registerTag('upper', {
|
||||
parse: function(tagToken, remainTokens) {
|
||||
this.str = tagToken.args; // name
|
||||
},
|
||||
render: function(scope, hash) {
|
||||
var str = Liquid.evalValue(this.str, scope); // 'alice'
|
||||
return Promise.resolve(str.toUpperCase()); // 'Alice'
|
||||
render: async function(scope, hash) {
|
||||
var str = await Liquid.evalValue(this.str, scope); // 'alice'
|
||||
return str.toUpperCase() // 'Alice'
|
||||
}
|
||||
});
|
||||
```
|
||||
@@ -228,7 +214,7 @@ engine.registerTag('upper', {
|
||||
* `parse`: Read tokens from `remainTokens` until your end token.
|
||||
* `render`: Combine scope data with your parsed tokens into HTML string.
|
||||
|
||||
See existing tag implementations here: <https://github.com/harttle/liquidjs/blob/master/tags/>
|
||||
See existing tag implementations here: <https://github.com/harttle/liquidjs/tree/master/src/builtin/tags>
|
||||
|
||||
## Plugin API
|
||||
|
||||
@@ -249,12 +235,28 @@ Plugin List:
|
||||
|
||||
* To add your plugin, contact me or simply send a PR.
|
||||
|
||||
## Differences With shopify/liquid
|
||||
|
||||
Though being compatible with [Ruby Liquid](https://github.com/shopify/liquid) is one of our priorities, there're still certain differences. You may need some configuration to get it compatible in these senarios:
|
||||
|
||||
* Dynamic file locating (enabled by default), that means layout/partial names are treated as variables in liquidjs. See [#51](https://github.com/harttle/liquidjs/issues/51).
|
||||
* Truthy and Falsy. All values except `undefined`, `null`, `false` are truthy, whereas in Ruby Liquid all except `nil` and `false` are truthy. See [#26](https://github.com/harttle/liquidjs/pull/26).
|
||||
* Number Rendering. Since JavaScript do not distinguish `float` and `integer`, we cannot either convert between them nor render regarding to their type. See [#59](https://github.com/harttle/liquidjs/issues/59).
|
||||
* [.to_liquid()](https://github.com/Shopify/liquid/wiki/Introduction-to-Drops) is replaced by `.toLiquid()`
|
||||
* [.to_s()](https://www.rubydoc.info/gems/liquid/Liquid/Drop) is replaced by JavaScript `.toString()`
|
||||
|
||||
## Contribute Guidelines
|
||||
|
||||
This repo uses [eslint](https://eslint.org/) to check code style, [semantic-release](https://github.com/semantic-release/semantic-release) to generate changelog and publish to npm and Github Releases.
|
||||
|
||||
* Code Style: <https://github.com/standard/eslint-config-standard>, `npm run lint` to check locally.
|
||||
* Commit Message: <https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#commits>
|
||||
|
||||
[nunjucks]: http://mozilla.github.io/nunjucks/
|
||||
[liquid-node]: https://github.com/sirlantis/liquid-node
|
||||
[shopify/liquid]: https://shopify.github.io/liquid/
|
||||
[jekyll]: http://jekyllrb.com/
|
||||
[gh]: https://pages.github.com/
|
||||
[releases]: https://github.com/harttle/liquidjs/releases
|
||||
[any-promise]: https://github.com/kevinbeaty/any-promise
|
||||
[test]: https://github.com/harttle/liquidjs/tree/master/test
|
||||
[caniuse-promises]: http://caniuse.com/#feat=promises
|
||||
@@ -263,3 +265,4 @@ Plugin List:
|
||||
[filters]: https://github.com/harttle/liquidjs/wiki/Builtin-Filters
|
||||
[express-views]: http://expressjs.com/en/guide/using-template-engines.html
|
||||
[pp]: https://github.com/taylorhakes/promise-polyfill
|
||||
[tutorial]: https://shopify.github.io/liquid/basics/introduction/
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"rules": {
|
||||
"no-unused-expressions": "off"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import * as Benchmark from 'benchmark'
|
||||
import Liquid from '../src/liquid'
|
||||
import TagToken from '../src/parser/tag-token'
|
||||
import Context from '../src/context/context'
|
||||
|
||||
const engine = new Liquid({
|
||||
root: __dirname,
|
||||
extname: '.liquid'
|
||||
})
|
||||
|
||||
engine.registerTag('header', {
|
||||
parse: function (token: TagToken) {
|
||||
const [key, val] = token.args.split(':')
|
||||
this[key] = val
|
||||
},
|
||||
render: function (ctx: Context) {
|
||||
const title = this.liquid.evalValue(this.content, ctx)
|
||||
return `<h1>${title}</h1>`
|
||||
}
|
||||
})
|
||||
|
||||
const ctx = {
|
||||
todos: ['fork and clone', 'make it better', 'make a pull request'],
|
||||
title: 'Welcome to liquidjs!'
|
||||
}
|
||||
|
||||
const template = `
|
||||
{%header content: "welcome to liquid" | capitalize%}
|
||||
|
||||
<ul>
|
||||
{% for todo in todos %}
|
||||
<li>{{forloop.index}} - {{todo}}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
`
|
||||
|
||||
export default function () {
|
||||
console.log('--- demo ---')
|
||||
return new Promise(resolve => {
|
||||
new Benchmark.Suite('demo')
|
||||
.add('demo', {
|
||||
defer: true,
|
||||
fn: (d: any) => engine.parseAndRender(template, ctx).then(x => d.resolve(x))
|
||||
})
|
||||
.on('cycle', (event: any) => console.log(String(event.target)))
|
||||
.on('complete', resolve)
|
||||
.run({ 'async': true })
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import output from './output'
|
||||
import tag from './tag'
|
||||
import demo from './demo'
|
||||
import layout from './layout'
|
||||
|
||||
async function main () {
|
||||
await output()
|
||||
await tag()
|
||||
await demo()
|
||||
await layout()
|
||||
}
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,36 @@
|
||||
import * as Benchmark from 'benchmark'
|
||||
import Liquid from '../src/liquid'
|
||||
|
||||
const engineOptions = {
|
||||
root: __dirname,
|
||||
extname: '.liquid'
|
||||
}
|
||||
|
||||
const engine = new Liquid(engineOptions)
|
||||
const cachingEngine = new Liquid({
|
||||
...engineOptions,
|
||||
cache: true
|
||||
})
|
||||
|
||||
const template = `
|
||||
{% layout "./templates/layout.liquid" %}
|
||||
{% block body %}a small body{% endblock %}
|
||||
`
|
||||
|
||||
export default function () {
|
||||
console.log('--- layout ---')
|
||||
return new Promise(resolve => {
|
||||
new Benchmark.Suite('layout')
|
||||
.add('cache=false', {
|
||||
defer: true,
|
||||
fn: (d: any) => engine.parseAndRender(template, {}).then(x => d.resolve(x))
|
||||
})
|
||||
.add('cache=true', {
|
||||
defer: true,
|
||||
fn: (d: any) => cachingEngine.parseAndRender(template, {}).then(x => d.resolve(x))
|
||||
})
|
||||
.on('cycle', (event: any) => console.log(String(event.target)))
|
||||
.on('complete', resolve)
|
||||
.run({ 'async': true })
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import * as Benchmark from 'benchmark'
|
||||
import Liquid from '../src/liquid'
|
||||
|
||||
const liquid = new Liquid()
|
||||
|
||||
export default function () {
|
||||
console.log('--- output ---')
|
||||
return new Promise(resolve => {
|
||||
new Benchmark.Suite('output')
|
||||
.add('literal', test('{{false}}{{"foo"}}{{32.322}}'))
|
||||
.add('truncate', test('{{"foobar" | truncate: 3}}'))
|
||||
.add('date', test('{{"now" | date: "%d%Y%m"}}'))
|
||||
.add('escape', test('{{"1<2" | escape}}'))
|
||||
.add('default', test('{{"" | default: 3}}'))
|
||||
.on('cycle', (event: any) => console.log(String(event.target)))
|
||||
.on('complete', resolve)
|
||||
.run({ 'async': true })
|
||||
})
|
||||
}
|
||||
|
||||
function test (str: string) {
|
||||
return {
|
||||
defer: true,
|
||||
fn: (d: any) => liquid.parseAndRender(str).then(x => d.resolve(x))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import * as Benchmark from 'benchmark'
|
||||
import Liquid from '../src/liquid'
|
||||
|
||||
const liquid = new Liquid()
|
||||
|
||||
export default function () {
|
||||
console.log('--- tag ---')
|
||||
return new Promise(resolve => {
|
||||
new Benchmark.Suite('tag')
|
||||
.add('if', test('{% if "foobar" %}foo{% endif %}'))
|
||||
.add('unless', test('{%unless "foo"%}true{%else%}false{%endunless%}'))
|
||||
.add('for', test('{% for i in (1..3) %}{{fooloop.index}}{% endfor %}'))
|
||||
.add('switch', test('{%case 3%}{% when 1 %}1{% when 2 %}2{% when 3 %}3{%endcase%}'))
|
||||
.add('assign', test('{%assign a="foo bar"%}'))
|
||||
.add('capture', test('{%capture foo%}what is this{%endcapture%}'))
|
||||
.add('increment', test('{%increment a%}'))
|
||||
.add('decrement', test('{%decrement a%}'))
|
||||
.add('tablerow', test('{%tablerow i in (1..10) cols:3%}{%endtablerow%}'))
|
||||
.on('cycle', (event: any) => console.log(String(event.target)))
|
||||
.on('complete', resolve)
|
||||
.run({ 'async': true })
|
||||
})
|
||||
}
|
||||
|
||||
function test (str: string) {
|
||||
return {
|
||||
defer: true,
|
||||
fn: (d: any) => liquid.parseAndRender(str).then(x => d.resolve(x))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
This is a barely empty layout with a body:
|
||||
{% block body %}{% endblock %}
|
||||
@@ -3,12 +3,12 @@
|
||||
|
||||
<head>
|
||||
<title>liquidjs for the browser</title>
|
||||
<script src="../../dist/liquid.js"></script>
|
||||
<script src="node_modules/liquidjs/dist/liquid.min.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script>
|
||||
var engine = window.Liquid({
|
||||
var engine = new window.Liquid({
|
||||
extname: '.html',
|
||||
cache: true
|
||||
});
|
||||
|
||||
Generated
+220
@@ -0,0 +1,220 @@
|
||||
{
|
||||
"name": "liquidjs-demo-browser",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"async": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
|
||||
"integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=",
|
||||
"dev": true
|
||||
},
|
||||
"colors": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz",
|
||||
"integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=",
|
||||
"dev": true
|
||||
},
|
||||
"corser": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz",
|
||||
"integrity": "sha1-jtolLsqrWEDc2XXOuQ2TcMgZ/4c=",
|
||||
"dev": true
|
||||
},
|
||||
"debug": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
|
||||
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ms": "2.1.1"
|
||||
}
|
||||
},
|
||||
"ecstatic": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/ecstatic/-/ecstatic-3.3.1.tgz",
|
||||
"integrity": "sha512-/rrctvxZ78HMI/tPIsqdvFKHHscxR3IJuKrZI2ZoUgkt2SiufyLFBmcco+aqQBIu6P1qBsUNG3drAAGLx80vTQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"he": "1.2.0",
|
||||
"mime": "1.6.0",
|
||||
"minimist": "1.2.0",
|
||||
"url-join": "2.0.5"
|
||||
}
|
||||
},
|
||||
"eventemitter3": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.0.tgz",
|
||||
"integrity": "sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==",
|
||||
"dev": true
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.7.0.tgz",
|
||||
"integrity": "sha512-m/pZQy4Gj287eNy94nivy5wchN3Kp+Q5WgUPNy5lJSZ3sgkVKSYV/ZChMAQVIgx1SqfZ2zBZtPA2YlXIWxxJOQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"debug": "3.2.6"
|
||||
}
|
||||
},
|
||||
"he": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
||||
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
|
||||
"dev": true
|
||||
},
|
||||
"http-proxy": {
|
||||
"version": "1.17.0",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.17.0.tgz",
|
||||
"integrity": "sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"eventemitter3": "3.1.0",
|
||||
"follow-redirects": "1.7.0",
|
||||
"requires-port": "1.0.0"
|
||||
}
|
||||
},
|
||||
"http-server": {
|
||||
"version": "0.11.1",
|
||||
"resolved": "https://registry.npmjs.org/http-server/-/http-server-0.11.1.tgz",
|
||||
"integrity": "sha512-6JeGDGoujJLmhjiRGlt8yK8Z9Kl0vnl/dQoQZlc4oeqaUoAKQg94NILLfrY3oWzSyFaQCVNTcKE5PZ3cH8VP9w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"colors": "1.0.3",
|
||||
"corser": "2.0.1",
|
||||
"ecstatic": "3.3.1",
|
||||
"http-proxy": "1.17.0",
|
||||
"opener": "1.4.3",
|
||||
"optimist": "0.6.1",
|
||||
"portfinder": "1.0.20",
|
||||
"union": "0.4.6"
|
||||
}
|
||||
},
|
||||
"liquidjs": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/liquidjs/-/liquidjs-7.0.0.tgz",
|
||||
"integrity": "sha512-/JL+VQUI++6iCS7JQ4fPPeji6vyvHCU/iuz2bLSiE0tn0eO3kf8GEyfaXnQUHSroy8SmJu7HQIvjvOErtYEvUg=="
|
||||
},
|
||||
"mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
||||
"dev": true
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
|
||||
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
|
||||
"dev": true
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
|
||||
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"minimist": "0.0.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"minimist": {
|
||||
"version": "0.0.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
|
||||
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
|
||||
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
|
||||
"dev": true
|
||||
},
|
||||
"opener": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/opener/-/opener-1.4.3.tgz",
|
||||
"integrity": "sha1-XG2ixdflgx6P+jlklQ+NZnSskLg=",
|
||||
"dev": true
|
||||
},
|
||||
"optimist": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
|
||||
"integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"minimist": "0.0.10",
|
||||
"wordwrap": "0.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"minimist": {
|
||||
"version": "0.0.10",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz",
|
||||
"integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"portfinder": {
|
||||
"version": "1.0.20",
|
||||
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.20.tgz",
|
||||
"integrity": "sha512-Yxe4mTyDzTd59PZJY4ojZR8F+E5e97iq2ZOHPz3HDgSvYC5siNad2tLooQ5y5QHyQhc3xVqvyk/eNA3wuoa7Sw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"async": "1.5.2",
|
||||
"debug": "2.6.9",
|
||||
"mkdirp": "0.5.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"qs": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-2.3.3.tgz",
|
||||
"integrity": "sha1-6eha2+ddoLvkyOBHaghikPhjtAQ=",
|
||||
"dev": true
|
||||
},
|
||||
"requires-port": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
|
||||
"dev": true
|
||||
},
|
||||
"union": {
|
||||
"version": "0.4.6",
|
||||
"resolved": "https://registry.npmjs.org/union/-/union-0.4.6.tgz",
|
||||
"integrity": "sha1-GY+9rrolTniLDvy2MLwR8kopWeA=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"qs": "2.3.3"
|
||||
}
|
||||
},
|
||||
"url-join": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/url-join/-/url-join-2.0.5.tgz",
|
||||
"integrity": "sha1-WvIvGMBSoACkjXuCxenC4v7tpyg=",
|
||||
"dev": true
|
||||
},
|
||||
"wordwrap": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
|
||||
"integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "liquidjs-demo-browser",
|
||||
"version": "1.0.0",
|
||||
"description": "Liquidjs Demo for Browser Usage",
|
||||
"main": "index.html",
|
||||
"scripts": {
|
||||
"start": "http-server -c-1 "
|
||||
},
|
||||
"author": "harttle <[email protected]>",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"http-server": "^0.11.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"liquidjs": "^7.0.0"
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
const express = require('express')
|
||||
const Liquid = require('../..')
|
||||
|
||||
let app = express()
|
||||
let engine = Liquid({
|
||||
const app = express()
|
||||
const engine = new Liquid({
|
||||
root: __dirname, // for layouts and partials
|
||||
extname: '.liquid'
|
||||
})
|
||||
@@ -12,7 +12,7 @@ app.set('views', ['./partials', './views']) // specify the views directory
|
||||
app.set('view engine', 'liquid') // set to default
|
||||
|
||||
app.get('/', function (req, res) {
|
||||
let todos = ['fork and clone', 'make it better', 'make a pull request']
|
||||
const todos = ['fork and clone', 'make it better', 'make a pull request']
|
||||
res.render('todolist', {
|
||||
todos: todos,
|
||||
title: 'Welcome to liquidjs!'
|
||||
|
||||
+16
-2
@@ -1,12 +1,26 @@
|
||||
const Liquid = require('../..')
|
||||
const Liquid = require('liquidjs')
|
||||
|
||||
const engine = new Liquid({
|
||||
root: __dirname,
|
||||
extname: '.liquid'
|
||||
})
|
||||
|
||||
engine.registerTag('header', {
|
||||
parse: function (token) {
|
||||
const [key, val] = token.args.split(':')
|
||||
this[key] = val
|
||||
},
|
||||
render: function (scope, hash) {
|
||||
const title = this.liquid.evalValue(this.content, scope)
|
||||
return `<h1>${title}</h1>`
|
||||
}
|
||||
})
|
||||
|
||||
const ctx = {
|
||||
todos: ['fork and clone', 'make it better', 'make a pull request'],
|
||||
title: 'Welcome to liquidjs!'
|
||||
}
|
||||
|
||||
engine.renderFile('todolist', ctx).then(console.log)
|
||||
engine.renderFile('todolist', ctx)
|
||||
.then(console.log)
|
||||
.catch(err => console.error(err.stack))
|
||||
|
||||
Generated
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Node.js demo for liquidjs",
|
||||
"requires": true,
|
||||
"lockfileVersion": 1,
|
||||
"dependencies": {
|
||||
"liquidjs": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/liquidjs/-/liquidjs-7.0.0.tgz",
|
||||
"integrity": "sha512-/JL+VQUI++6iCS7JQ4fPPeji6vyvHCU/iuz2bLSiE0tn0eO3kf8GEyfaXnQUHSroy8SmJu7HQIvjvOErtYEvUg=="
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "liquidjs-demo-nodejs",
|
||||
"description": "Node.js demo for liquidjs",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"liquidjs": "^7.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
{%header content: "welcome to liquid" | capitalize%}
|
||||
|
||||
<ul>
|
||||
{% for todo in todos %}
|
||||
<li>{{forloop.index}} - {{todo}}</li>
|
||||
|
||||
@@ -14,7 +14,7 @@ class App extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
|
||||
let engine = Liquid({
|
||||
let engine = new Liquid({
|
||||
root: path.resolve(__dirname, 'views/'), // dirs to lookup layouts/includes
|
||||
extname: '.liquid' // the extname used for layouts/includes, defaults
|
||||
});
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import Liquid from 'liquidjs'
|
||||
|
||||
const engine = new Liquid({
|
||||
root: __dirname,
|
||||
extname: '.liquid'
|
||||
})
|
||||
|
||||
engine.registerTag('header', {
|
||||
parse: function (token) {
|
||||
const [key, val] = token.args.split(':')
|
||||
this[key] = val
|
||||
},
|
||||
render: function (scope, hash) {
|
||||
const title = this.liquid.evalValue(this['content'], scope)
|
||||
return `<h1>${title}</h1>`
|
||||
}
|
||||
})
|
||||
|
||||
const ctx = {
|
||||
todos: ['fork and clone', 'make it better', 'make a pull request'],
|
||||
title: 'Welcome to liquidjs!'
|
||||
}
|
||||
|
||||
// console.log('isTruthy:', isTruthy('a string here'));
|
||||
engine.renderFile('todolist', ctx).then(console.log)
|
||||
Generated
+8701
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "liquidjs-demo-typescript",
|
||||
"description": "TypeScript demo for liquidjs",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"start": "ts-node index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"liquidjs": "^7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ts-node": "^8.0.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{%header content: "welcome to liquid" | capitalize%}
|
||||
|
||||
<ul>
|
||||
{% for todo in todos %}
|
||||
<li>{{forloop.index}} - {{todo}}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
Vendored
-3632
File diff suppressed because it is too large
Load Diff
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
-3580
File diff suppressed because it is too large
Load Diff
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
-2
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,6 @@
|
||||
declare module 'rollup-plugin-uglify' {
|
||||
export function uglify (): any
|
||||
}
|
||||
declare module 'rollup-plugin-replace' {
|
||||
export default function replace(options: any): any
|
||||
}
|
||||
Generated
+7186
-2843
File diff suppressed because it is too large
Load Diff
+71
-72
@@ -1,29 +1,31 @@
|
||||
{
|
||||
"name": "liquidjs",
|
||||
"version": "6.3.0",
|
||||
"version": "8.3.0",
|
||||
"description": "Liquid template engine by pure JavaScript: compatible to shopify, easy to extend.",
|
||||
"main": "dist/liquid.common.js",
|
||||
"module": "src/index.js",
|
||||
"types": "dist/liquid.d.ts",
|
||||
"browser": "dist/liquid.js",
|
||||
"types": "src/index.d.ts",
|
||||
"scripts": {
|
||||
"lint": "eslint src/ test/ *.js",
|
||||
"dev": "mocha test/unit",
|
||||
"test": "cross-env NODE_ENV=test nyc --reporter=html mocha test/unit",
|
||||
"e2e": "mocha test/e2e",
|
||||
"coveralls": "nyc report --reporter=text-lcov | coveralls",
|
||||
"dist": "rollup -c && ls -lh dist",
|
||||
"demo:browser": "echo open http://localhost:8080/demo/browser && http-server -c-1",
|
||||
"demo:nodejs": "node ./demo/nodejs/index.js",
|
||||
"demo:express": "cd ./demo/express/ && npm start",
|
||||
"preversion": "npm run lint && npm test",
|
||||
"version": "npm run dist && git add -A dist",
|
||||
"postversion": "git push --tags"
|
||||
"lint": "eslint . --ext .ts",
|
||||
"unit": "mocha \"test/unit/**/*.ts\"",
|
||||
"integration": "mocha \"test/integration/**/*.ts\"",
|
||||
"e2e": "npm run build && mocha \"test/e2e/**/*.ts\"",
|
||||
"test": "cross-env BUNDLES=cjs,umd npm run build && mocha \"test/**/*.ts\"",
|
||||
"benchmark": "ts-node benchmark",
|
||||
"coverage-html": "nyc --reporter=html mocha \"test/{unit,integration}/**/*.ts\"",
|
||||
"coverage-coveralls": "nyc mocha \"test/{unit,integration}/**/*.ts\" && nyc report --reporter=text-lcov | coveralls",
|
||||
"build": "rollup -c rollup.config.ts && ls -lh dist",
|
||||
"version": "npm run build"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/harttle/liquidjs.git"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"LICENSE",
|
||||
"README.md"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4.8.7"
|
||||
},
|
||||
@@ -41,68 +43,65 @@
|
||||
},
|
||||
"homepage": "https://github.com/harttle/liquidjs#readme",
|
||||
"devDependencies": {
|
||||
"babel-core": "^6.26.3",
|
||||
"babel-plugin-external-helpers": "^6.22.0",
|
||||
"babel-plugin-istanbul": "^4.1.6",
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"chai": "^4.1.2",
|
||||
"@semantic-release/changelog": "^3.0.2",
|
||||
"@semantic-release/commit-analyzer": "^6.1.0",
|
||||
"@semantic-release/git": "^7.0.8",
|
||||
"@semantic-release/npm": "^5.1.8",
|
||||
"@semantic-release/release-notes-generator": "^7.1.4",
|
||||
"@types/benchmark": "^1.0.31",
|
||||
"@types/chai": "^4.1.7",
|
||||
"@types/chai-as-promised": "^7.1.0",
|
||||
"@types/express": "^4.16.1",
|
||||
"@types/jsdom": "^12.2.2",
|
||||
"@types/mocha": "^5.2.6",
|
||||
"@types/sinon": "^7.0.6",
|
||||
"@types/sinon-chai": "^3.2.2",
|
||||
"@types/supertest": "^2.0.7",
|
||||
"@typescript-eslint/eslint-plugin": "^1.3.0",
|
||||
"benchmark": "^2.1.4",
|
||||
"chai": "^4.2.0",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"coveralls": "^3.0.0",
|
||||
"coveralls": "^3.0.2",
|
||||
"cross-env": "^5.2.0",
|
||||
"eslint": "^5.2.0",
|
||||
"eslint-config-standard": "^11.0.0",
|
||||
"eslint-plugin-import": "^2.7.0",
|
||||
"eslint-plugin-mocha": "^5.1.0",
|
||||
"eslint-plugin-node": "^7.0.1",
|
||||
"eslint-plugin-promise": "^3.5.0",
|
||||
"eslint-plugin-standard": "^3.0.1",
|
||||
"express": "^4.16.1",
|
||||
"http-server": "^0.11.1",
|
||||
"jsdom": "^11.5.1",
|
||||
"eslint": "^5.12.1",
|
||||
"eslint-config-standard": "^12.0.0",
|
||||
"eslint-plugin-import": "^2.15.0",
|
||||
"eslint-plugin-mocha": "^5.3.0",
|
||||
"eslint-plugin-node": "^8.0.1",
|
||||
"eslint-plugin-promise": "^4.0.1",
|
||||
"eslint-plugin-standard": "^4.0.0",
|
||||
"express": "^4.16.4",
|
||||
"jsdom": "^13.2.0",
|
||||
"mocha": "^5.2.0",
|
||||
"mock-fs": "^4.4.1",
|
||||
"nyc": "^13.0.1",
|
||||
"nyc": "^13.1.0",
|
||||
"regenerator-runtime": "^0.12.1",
|
||||
"rollup": "^0.64.1",
|
||||
"rollup-plugin-alias": "^1.4.0",
|
||||
"rollup-plugin-babel": "^3.0.7",
|
||||
"rollup-plugin-node-resolve": "^3.3.0",
|
||||
"rollup-plugin-shim": "^1.0.0",
|
||||
"rollup-plugin-uglify": "^4.0.0",
|
||||
"sinon": "^6.1.4",
|
||||
"sinon-chai": "^3.2.0",
|
||||
"supertest": "^3.0.0"
|
||||
"rollup": "^1.1.2",
|
||||
"rollup-plugin-replace": "^2.1.0",
|
||||
"rollup-plugin-typescript2": "^0.21.1",
|
||||
"rollup-plugin-uglify": "^6.0.2",
|
||||
"semantic-release": "^15.13.14",
|
||||
"sinon": "^7.2.3",
|
||||
"sinon-chai": "^3.3.0",
|
||||
"supertest": "^3.4.2",
|
||||
"ts-node": "^8.0.2",
|
||||
"tslib": "^1.9.3",
|
||||
"typescript": "^3.3.3"
|
||||
},
|
||||
"release": {
|
||||
"branch": "master",
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer",
|
||||
"@semantic-release/release-notes-generator",
|
||||
"@semantic-release/changelog",
|
||||
"@semantic-release/npm",
|
||||
"@semantic-release/git",
|
||||
"@semantic-release/github"
|
||||
]
|
||||
},
|
||||
"nyc": {
|
||||
"require": [
|
||||
"babel-core/register"
|
||||
],
|
||||
"sourceMap": false,
|
||||
"instrument": false
|
||||
"extension": [
|
||||
".ts"
|
||||
]
|
||||
},
|
||||
"babel": {
|
||||
"presets": [
|
||||
"env"
|
||||
],
|
||||
"plugins": [
|
||||
[
|
||||
"transform-runtime",
|
||||
{
|
||||
"helpers": false,
|
||||
"polyfill": false,
|
||||
"regenerator": true,
|
||||
"moduleName": "babel-runtime"
|
||||
}
|
||||
]
|
||||
],
|
||||
"env": {
|
||||
"test": {
|
||||
"plugins": [
|
||||
"istanbul"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
"dependencies": {}
|
||||
}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import shim from 'rollup-plugin-shim'
|
||||
import alias from 'rollup-plugin-alias'
|
||||
import babel from 'rollup-plugin-babel'
|
||||
import {uglify} from 'rollup-plugin-uglify'
|
||||
import pkg from './package.json'
|
||||
import nodeResolve from 'rollup-plugin-node-resolve'
|
||||
|
||||
const fake = {fs: `export default {}`, path: `export default {}`}
|
||||
const version = process.env.VERSION || pkg.version
|
||||
const sourcemap = true
|
||||
const banner = `/*
|
||||
* liquidjs@${version}, https://github.com/harttle/liquidjs
|
||||
* (c) 2016-${new Date().getFullYear()} harttle
|
||||
* Released under the MIT License.
|
||||
*/`
|
||||
const treeshake = {
|
||||
propertyReadSideEffects: false
|
||||
}
|
||||
const input = 'src/index.js'
|
||||
|
||||
const babelConf = {
|
||||
babelrc: false,
|
||||
'presets': [['env', {'modules': false}]],
|
||||
'plugins': ['external-helpers']
|
||||
}
|
||||
|
||||
export default [{
|
||||
output: [{
|
||||
file: 'dist/liquid.common.js',
|
||||
name: 'Liquid',
|
||||
format: 'cjs',
|
||||
sourcemap,
|
||||
banner
|
||||
}],
|
||||
external: ['path', 'fs'],
|
||||
plugins: [
|
||||
nodeResolve(),
|
||||
babel(babelConf)
|
||||
],
|
||||
treeshake,
|
||||
input
|
||||
}, {
|
||||
output: [{
|
||||
file: 'dist/liquid.js',
|
||||
name: 'Liquid',
|
||||
format: 'umd',
|
||||
sourcemap,
|
||||
banner
|
||||
}],
|
||||
plugins: [
|
||||
shim(fake),
|
||||
alias({
|
||||
'./template': './template-browser'
|
||||
}),
|
||||
nodeResolve(),
|
||||
babel(babelConf)
|
||||
],
|
||||
treeshake,
|
||||
input
|
||||
}, {
|
||||
output: [{
|
||||
file: 'dist/liquid.min.js',
|
||||
name: 'Liquid',
|
||||
format: 'umd',
|
||||
sourcemap
|
||||
}],
|
||||
plugins: [
|
||||
shim(fake),
|
||||
nodeResolve(),
|
||||
babel(babelConf),
|
||||
uglify()
|
||||
],
|
||||
treeshake,
|
||||
input
|
||||
}]
|
||||
@@ -0,0 +1,106 @@
|
||||
import { uglify } from 'rollup-plugin-uglify'
|
||||
import pkg from './package.json'
|
||||
import typescript from 'rollup-plugin-typescript2'
|
||||
import replace from 'rollup-plugin-replace'
|
||||
|
||||
const version = process.env.VERSION || pkg.version
|
||||
const sourcemap = true
|
||||
const banner = `/*
|
||||
* liquidjs@${version}, https://github.com/harttle/liquidjs
|
||||
* (c) 2016-${new Date().getFullYear()} harttle
|
||||
* Released under the MIT License.
|
||||
*/`
|
||||
const treeshake = {
|
||||
propertyReadSideEffects: false
|
||||
}
|
||||
const input = './src/liquid.ts'
|
||||
|
||||
const cjs = {
|
||||
output: [{
|
||||
file: 'dist/liquid.common.js',
|
||||
name: 'Liquid',
|
||||
format: 'cjs',
|
||||
sourcemap,
|
||||
banner
|
||||
}],
|
||||
external: ['path', 'fs'],
|
||||
plugins: [typescript({
|
||||
tsconfigOverride: {
|
||||
include: [ 'src' ],
|
||||
exclude: [ 'test', 'benchmark' ],
|
||||
compilerOptions: {
|
||||
target: 'es5',
|
||||
module: 'ES2015'
|
||||
}
|
||||
}
|
||||
})],
|
||||
treeshake,
|
||||
input
|
||||
}
|
||||
|
||||
const umd = {
|
||||
output: [{
|
||||
file: 'dist/liquid.js',
|
||||
name: 'Liquid',
|
||||
format: 'umd',
|
||||
sourcemap,
|
||||
banner
|
||||
}],
|
||||
plugins: [
|
||||
replace({
|
||||
include: './src/liquid.ts',
|
||||
delimiters: ['', ''],
|
||||
'./fs/node': './fs/browser'
|
||||
}),
|
||||
typescript({
|
||||
tsconfigOverride: {
|
||||
include: [ 'src' ],
|
||||
exclude: [ 'test', 'benchmark' ],
|
||||
compilerOptions: {
|
||||
target: 'es5',
|
||||
module: 'ES2015'
|
||||
}
|
||||
}
|
||||
})
|
||||
],
|
||||
treeshake,
|
||||
input
|
||||
}
|
||||
|
||||
const min = {
|
||||
output: [{
|
||||
file: 'dist/liquid.min.js',
|
||||
name: 'Liquid',
|
||||
format: 'umd',
|
||||
sourcemap
|
||||
}],
|
||||
plugins: [
|
||||
replace({
|
||||
include: './src/liquid.ts',
|
||||
delimiters: ['', ''],
|
||||
'./fs/node': './fs/browser'
|
||||
}),
|
||||
typescript({
|
||||
tsconfigOverride: {
|
||||
include: [ 'src' ],
|
||||
exclude: [ 'test', 'benchmark' ],
|
||||
compilerOptions: {
|
||||
target: 'es5',
|
||||
module: 'ES2015'
|
||||
}
|
||||
}
|
||||
}),
|
||||
uglify()
|
||||
],
|
||||
treeshake,
|
||||
input
|
||||
}
|
||||
|
||||
const bundles = []
|
||||
const env = process.env.BUNDLES || ''
|
||||
if (env.includes('cjs')) bundles.push(cjs)
|
||||
if (env.includes('umd')) bundles.push(umd)
|
||||
if (env.includes('min')) bundles.push(min)
|
||||
if (bundles.length === 0) bundles.push(cjs, umd, min)
|
||||
|
||||
export default bundles
|
||||
@@ -0,0 +1,28 @@
|
||||
import { last } from '../../util/underscore'
|
||||
import { isTruthy } from '../../render/syntax'
|
||||
|
||||
export default {
|
||||
'join': (v: any[], arg: string) => v.join(arg === undefined ? ' ' : arg),
|
||||
'last': <T>(v: T[]): T => last(v),
|
||||
'first': <T>(v: T[]): T => v[0],
|
||||
'map': <T1, T2>(arr: {[key: string]: T1}[], arg: string): T1[] => arr.map(v => v[arg]),
|
||||
'reverse': (v: any[]) => [...v].reverse(),
|
||||
'sort': <T>(v: T[], arg: (lhs: T, rhs: T) => number) => v.sort(arg),
|
||||
'size': (v: string | any[]) => v.length,
|
||||
'concat': <T1, T2>(v: T1[], arg: T2[] | T2): Array<T1 | T2> => Array.prototype.concat.call(v, arg),
|
||||
'slice': <T>(v: T[], begin: number, length: number = 1): T[] => {
|
||||
begin = begin < 0 ? v.length + begin : begin
|
||||
return v.slice(begin, begin + length)
|
||||
},
|
||||
'uniq': function<T> (arr: T[]): T[] {
|
||||
const u = {}
|
||||
return (arr || []).filter(val => {
|
||||
if (u.hasOwnProperty(String(val))) return false
|
||||
u[String(val)] = true
|
||||
return true
|
||||
})
|
||||
},
|
||||
'where': function<T> (arr: T[], property: string, value?: any): T[] {
|
||||
return arr.filter(obj => value === undefined ? isTruthy(obj[property]) : obj[property] === value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import strftime from '../../util/strftime'
|
||||
import { isString, isNumber } from '../../util/underscore'
|
||||
|
||||
export default {
|
||||
'date': (v: string | Date, arg: string) => {
|
||||
let date = v
|
||||
if (v === 'now') {
|
||||
date = new Date()
|
||||
} else if (isNumber(v)) {
|
||||
date = new Date(v * 1000)
|
||||
} else if (isString(v)) {
|
||||
date = /^\d+$/.test(v) ? new Date(+v * 1000) : new Date(v)
|
||||
}
|
||||
return isValidDate(date) ? strftime(date, arg) : v
|
||||
}
|
||||
}
|
||||
|
||||
function isValidDate (date: any): date is Date {
|
||||
return date instanceof Date && !isNaN(date.getTime())
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
const escapeMap = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
}
|
||||
const unescapeMap = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
''': "'"
|
||||
}
|
||||
|
||||
function escape (str: string) {
|
||||
return String(str).replace(/&|<|>|"|'/g, m => escapeMap[m])
|
||||
}
|
||||
|
||||
function unescape (str: string) {
|
||||
return String(str).replace(/&(amp|lt|gt|#34|#39);/g, m => unescapeMap[m])
|
||||
}
|
||||
|
||||
export default {
|
||||
'escape': escape,
|
||||
'escape_once': (str: string) => escape(unescape(str)),
|
||||
'newline_to_br': (v: string) => v.replace(/\n/g, '<br />'),
|
||||
'strip_html': (v: string) => v.replace(/<script.*?<\/script>|<!--.*?-->|<style.*?<\/style>|<.*?>/g, '')
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import html from './html'
|
||||
import str from './string'
|
||||
import math from './math'
|
||||
import url from './url'
|
||||
import array from './array'
|
||||
import date from './date'
|
||||
import obj from './object'
|
||||
|
||||
export default { ...html, ...str, ...math, ...url, ...date, ...obj, ...array }
|
||||
@@ -0,0 +1,14 @@
|
||||
export default {
|
||||
'abs': (v: number) => Math.abs(v),
|
||||
'ceil': (v: number) => Math.ceil(v),
|
||||
'divided_by': (v: number, arg: number) => v / arg,
|
||||
'floor': (v: number) => Math.floor(v),
|
||||
'minus': (v: number, arg: number) => v - arg,
|
||||
'modulo': (v: number, arg: number) => v % arg,
|
||||
'round': (v: number, arg: number = 0) => {
|
||||
const amp = Math.pow(10, arg)
|
||||
return Math.round(v * amp) / amp
|
||||
},
|
||||
'plus': (v: number, arg: number) => Number(v) + Number(arg),
|
||||
'times': (v: number, arg: number) => v * arg
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { isFalsy } from '../../render/syntax'
|
||||
import { toValue } from '../../util/underscore'
|
||||
|
||||
export default {
|
||||
'default': function<T1, T2> (v: string | T1, arg: T2): string | T1 | T2 {
|
||||
return isFalsy(toValue(v)) || v === '' ? arg : v
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
export default {
|
||||
'append': (v: string, arg: string) => v + arg,
|
||||
'prepend': (v: string, arg: string) => arg + v,
|
||||
'capitalize': (str: string) => String(str).charAt(0).toUpperCase() + str.slice(1),
|
||||
'lstrip': (v: string) => String(v).replace(/^\s+/, ''),
|
||||
'downcase': (v: string) => v.toLowerCase(),
|
||||
'upcase': (str: string) => String(str).toUpperCase(),
|
||||
'remove': (v: string, arg: string) => v.split(arg).join(''),
|
||||
'remove_first': (v: string, l: string) => v.replace(l, ''),
|
||||
'replace': (v: string, pattern: string, replacement: string) =>
|
||||
String(v).split(pattern).join(replacement),
|
||||
'replace_first': (v: string, arg1: string, arg2: string) => String(v).replace(arg1, arg2),
|
||||
'rstrip': (str: string) => String(str).replace(/\s+$/, ''),
|
||||
'split': (v: string, arg: string) => String(v).split(arg),
|
||||
'strip': (v: string) => String(v).trim(),
|
||||
'strip_newlines': (v: string) => String(v).replace(/\n/g, ''),
|
||||
'truncate': (v: string, l: number = 50, o: string = '...') => {
|
||||
v = String(v)
|
||||
if (v.length <= l) return v
|
||||
return v.substr(0, l - o.length) + o
|
||||
},
|
||||
'truncatewords': (v: string, l: number = 15, o: string = '...') => {
|
||||
const arr = v.split(/\s+/)
|
||||
let ret = arr.slice(0, l).join(' ')
|
||||
if (arr.length >= l) ret += o
|
||||
return ret
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export default {
|
||||
'url_decode': (x: string) => x.split('+').map(decodeURIComponent).join(' '),
|
||||
'url_encode': (x: string) => x.split(' ').map(encodeURIComponent).join('+')
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import assert from '../../util/assert'
|
||||
import { identifier } from '../../parser/lexical'
|
||||
import TagToken from '../../parser/tag-token'
|
||||
import Context from '../../context/context'
|
||||
import ITagImplOptions from '../../template/tag/itag-impl-options'
|
||||
|
||||
const re = new RegExp(`(${identifier.source})\\s*=([^]*)`)
|
||||
|
||||
export default {
|
||||
parse: function (token: TagToken) {
|
||||
const match = token.args.match(re) as RegExpMatchArray
|
||||
assert(match, `illegal token ${token.raw}`)
|
||||
this.key = match[1]
|
||||
this.value = match[2]
|
||||
},
|
||||
render: async function (ctx: Context) {
|
||||
ctx.front()[this.key] = await this.liquid.evalValue(this.value, ctx)
|
||||
}
|
||||
} as ITagImplOptions
|
||||
@@ -0,0 +1,35 @@
|
||||
import BlockMode from '../../context/block-mode'
|
||||
import TagToken from '../../parser/tag-token'
|
||||
import Token from '../../parser/token'
|
||||
import ITemplate from '../../template/itemplate'
|
||||
import Context from '../../context/context'
|
||||
import ITagImplOptions from '../../template/tag/itag-impl-options'
|
||||
import ParseStream from '../../parser/parse-stream'
|
||||
|
||||
export default {
|
||||
parse: function (token: TagToken, remainTokens: Token[]) {
|
||||
const match = /\w+/.exec(token.args)
|
||||
this.block = match ? match[0] : ''
|
||||
this.tpls = [] as ITemplate[]
|
||||
const stream: ParseStream = this.liquid.parser.parseStream(remainTokens)
|
||||
.on('tag:endblock', () => stream.stop())
|
||||
.on('template', (tpl: ITemplate) => this.tpls.push(tpl))
|
||||
.on('end', () => {
|
||||
throw new Error(`tag ${token.raw} not closed`)
|
||||
})
|
||||
stream.start()
|
||||
},
|
||||
render: async function (ctx: Context) {
|
||||
const blocks = ctx.getRegister('blocks')
|
||||
const childDefined = blocks[this.block]
|
||||
const html = childDefined !== undefined
|
||||
? childDefined
|
||||
: await this.liquid.renderer.renderTemplates(this.tpls, ctx)
|
||||
|
||||
if (ctx.getRegister('blockMode', BlockMode.OUTPUT) === BlockMode.STORE) {
|
||||
blocks[this.block] = html
|
||||
return ''
|
||||
}
|
||||
return html
|
||||
}
|
||||
} as ITagImplOptions
|
||||
@@ -0,0 +1,7 @@
|
||||
import { RenderBreakError } from '../../util/error'
|
||||
|
||||
export default {
|
||||
render: async function () {
|
||||
throw new RenderBreakError('break')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import assert from '../../util/assert'
|
||||
import { identifier } from '../../parser/lexical'
|
||||
import TagToken from '../../parser/tag-token'
|
||||
import Token from '../../parser/token'
|
||||
import Context from '../../context/context'
|
||||
import ITagImplOptions from '../../template/tag/itag-impl-options'
|
||||
|
||||
const re = new RegExp(`(${identifier.source})`)
|
||||
|
||||
export default {
|
||||
parse: function (tagToken: TagToken, remainTokens: Token[]) {
|
||||
const match = tagToken.args.match(re) as RegExpMatchArray
|
||||
assert(match, `${tagToken.args} not valid identifier`)
|
||||
|
||||
this.variable = match[1]
|
||||
this.templates = []
|
||||
|
||||
const stream = this.liquid.parser.parseStream(remainTokens)
|
||||
stream.on('tag:endcapture', () => stream.stop())
|
||||
.on('template', (tpl) => this.templates.push(tpl))
|
||||
.on('end', () => {
|
||||
throw new Error(`tag ${tagToken.raw} not closed`)
|
||||
})
|
||||
stream.start()
|
||||
},
|
||||
render: async function (ctx: Context) {
|
||||
const html = await this.liquid.renderer.renderTemplates(this.templates, ctx)
|
||||
ctx.front()[this.variable] = html
|
||||
}
|
||||
} as ITagImplOptions
|
||||
@@ -0,0 +1,44 @@
|
||||
import { evalExp } from '../../render/syntax'
|
||||
import TagToken from '../../parser/tag-token'
|
||||
import Token from '../../parser/token'
|
||||
import Context from '../../context/context'
|
||||
import ITemplate from '../../template/itemplate'
|
||||
import ITagImplOptions from '../../template/tag/itag-impl-options'
|
||||
import ParseStream from '../../parser/parse-stream'
|
||||
|
||||
export default {
|
||||
parse: function (tagToken: TagToken, remainTokens: Token[]) {
|
||||
this.cond = tagToken.args
|
||||
this.cases = []
|
||||
this.elseTemplates = []
|
||||
|
||||
let p: ITemplate[] = []
|
||||
const stream: ParseStream = this.liquid.parser.parseStream(remainTokens)
|
||||
.on('tag:when', (token: TagToken) => {
|
||||
this.cases.push({
|
||||
val: token.args,
|
||||
templates: p = []
|
||||
})
|
||||
})
|
||||
.on('tag:else', () => (p = this.elseTemplates))
|
||||
.on('tag:endcase', () => stream.stop())
|
||||
.on('template', (tpl: ITemplate) => p.push(tpl))
|
||||
.on('end', () => {
|
||||
throw new Error(`tag ${tagToken.raw} not closed`)
|
||||
})
|
||||
|
||||
stream.start()
|
||||
},
|
||||
|
||||
render: async function (ctx: Context) {
|
||||
for (let i = 0; i < this.cases.length; i++) {
|
||||
const branch = this.cases[i]
|
||||
const val = await evalExp(branch.val, ctx)
|
||||
const cond = await evalExp(this.cond, ctx)
|
||||
if (val === cond) {
|
||||
return this.liquid.renderer.renderTemplates(branch.templates, ctx)
|
||||
}
|
||||
}
|
||||
return this.liquid.renderer.renderTemplates(this.elseTemplates, ctx)
|
||||
}
|
||||
} as ITagImplOptions
|
||||
@@ -0,0 +1,17 @@
|
||||
import TagToken from '../../parser/tag-token'
|
||||
import Token from '../../parser/token'
|
||||
import ITagImplOptions from '../../template/tag/itag-impl-options'
|
||||
|
||||
export default {
|
||||
parse: function (tagToken: TagToken, remainTokens: Token[]) {
|
||||
const stream = this.liquid.parser.parseStream(remainTokens)
|
||||
stream
|
||||
.on('token', (token: TagToken) => {
|
||||
if (token.name === 'endcomment') stream.stop()
|
||||
})
|
||||
.on('end', () => {
|
||||
throw new Error(`tag ${tagToken.raw} not closed`)
|
||||
})
|
||||
stream.start()
|
||||
}
|
||||
} as ITagImplOptions
|
||||
@@ -0,0 +1,7 @@
|
||||
import { RenderBreakError } from '../../util/error'
|
||||
|
||||
export default {
|
||||
render: async function () {
|
||||
throw new RenderBreakError('continue')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import assert from '../../util/assert'
|
||||
import { value as rValue } from '../../parser/lexical'
|
||||
import { evalValue } from '../../render/syntax'
|
||||
import TagToken from '../../parser/tag-token'
|
||||
import Context from '../../context/context'
|
||||
import ITagImplOptions from '../../template/tag/itag-impl-options'
|
||||
|
||||
const groupRE = new RegExp(`^(?:(${rValue.source})\\s*:\\s*)?(.*)$`)
|
||||
const candidatesRE = new RegExp(rValue.source, 'g')
|
||||
|
||||
export default <ITagImplOptions>{
|
||||
parse: function (tagToken: TagToken) {
|
||||
let match: RegExpExecArray | null = groupRE.exec(tagToken.args) as RegExpExecArray
|
||||
assert(match, `illegal tag: ${tagToken.raw}`)
|
||||
|
||||
this.group = match[1] || ''
|
||||
const candidates = match[2]
|
||||
|
||||
this.candidates = []
|
||||
|
||||
while ((match = candidatesRE.exec(candidates))) {
|
||||
this.candidates.push(match[0])
|
||||
}
|
||||
assert(this.candidates.length, `empty candidates: ${tagToken.raw}`)
|
||||
},
|
||||
|
||||
render: async function (ctx: Context) {
|
||||
const group = await evalValue(this.group, ctx)
|
||||
const fingerprint = `cycle:${group}:` + this.candidates.join(',')
|
||||
const groups = ctx.getRegister('cycle')
|
||||
let idx = groups[fingerprint]
|
||||
|
||||
if (idx === undefined) {
|
||||
idx = groups[fingerprint] = 0
|
||||
}
|
||||
|
||||
const candidate = this.candidates[idx]
|
||||
idx = (idx + 1) % this.candidates.length
|
||||
groups[fingerprint] = idx
|
||||
|
||||
return evalValue(candidate, ctx)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import assert from '../../util/assert'
|
||||
import { identifier } from '../../parser/lexical'
|
||||
import TagToken from '../../parser/tag-token'
|
||||
import Context from '../../context/context'
|
||||
import ITagImplOptions from '../../template/tag/itag-impl-options'
|
||||
import { isNumber } from '../../util/underscore'
|
||||
|
||||
export default {
|
||||
parse: function (token: TagToken) {
|
||||
const match = token.args.match(identifier) as RegExpMatchArray
|
||||
assert(match, `illegal identifier ${token.args}`)
|
||||
this.variable = match[0]
|
||||
},
|
||||
render: function (context: Context) {
|
||||
const scope = context.environments
|
||||
if (!isNumber(scope[this.variable])) {
|
||||
scope[this.variable] = 0
|
||||
}
|
||||
return --scope[this.variable]
|
||||
}
|
||||
} as ITagImplOptions
|
||||
@@ -0,0 +1,82 @@
|
||||
import { isString, isObject, isArray } from '../../util/underscore'
|
||||
import { parseExp } from '../../render/syntax'
|
||||
import assert from '../../util/assert'
|
||||
import { identifier, value, hash } from '../../parser/lexical'
|
||||
import TagToken from '../../parser/tag-token'
|
||||
import Token from '../../parser/token'
|
||||
import Context from '../../context/context'
|
||||
import Hash from '../../template/tag/hash'
|
||||
import ITemplate from '../../template/itemplate'
|
||||
import ITagImplOptions from '../../template/tag/itag-impl-options'
|
||||
import ParseStream from '../../parser/parse-stream'
|
||||
import { ForloopDrop } from '../../drop/forloop-drop'
|
||||
|
||||
const re = new RegExp(`^(${identifier.source})\\s+in\\s+` +
|
||||
`(${value.source})` +
|
||||
`(?:\\s+${hash.source})*` +
|
||||
`(?:\\s+(reversed))?` +
|
||||
`(?:\\s+${hash.source})*$`)
|
||||
|
||||
export default <ITagImplOptions>{
|
||||
type: 'block',
|
||||
parse: function (tagToken: TagToken, remainTokens: Token[]) {
|
||||
const match = re.exec(tagToken.args) as RegExpExecArray
|
||||
assert(match, `illegal tag: ${tagToken.raw}`)
|
||||
this.variable = match[1]
|
||||
this.collection = match[2]
|
||||
this.reversed = !!match[3]
|
||||
|
||||
this.templates = []
|
||||
this.elseTemplates = []
|
||||
|
||||
let p
|
||||
const stream: ParseStream = this.liquid.parser.parseStream(remainTokens)
|
||||
.on('start', () => (p = this.templates))
|
||||
.on('tag:else', () => (p = this.elseTemplates))
|
||||
.on('tag:endfor', () => stream.stop())
|
||||
.on('template', (tpl: ITemplate) => p.push(tpl))
|
||||
.on('end', () => {
|
||||
throw new Error(`tag ${tagToken.raw} not closed`)
|
||||
})
|
||||
|
||||
stream.start()
|
||||
},
|
||||
render: async function (ctx: Context, hash: Hash) {
|
||||
let collection = await parseExp(this.collection, ctx)
|
||||
|
||||
if (!isArray(collection)) {
|
||||
if (isString(collection) && collection.length > 0) {
|
||||
collection = [collection] as string[]
|
||||
} else if (isObject(collection)) {
|
||||
collection = Object.keys(collection).map((key) => [key, collection[key]])
|
||||
}
|
||||
}
|
||||
if (!isArray(collection) || !collection.length) {
|
||||
return this.liquid.renderer.renderTemplates(this.elseTemplates, ctx)
|
||||
}
|
||||
|
||||
const offset = hash.offset || 0
|
||||
const limit = (hash.limit === undefined) ? collection.length : hash.limit
|
||||
|
||||
collection = collection.slice(offset, offset + limit)
|
||||
if (this.reversed) collection.reverse()
|
||||
|
||||
const context = { forloop: new ForloopDrop(collection.length) }
|
||||
ctx.push(context)
|
||||
let html = ''
|
||||
for (const item of collection) {
|
||||
context[this.variable] = item
|
||||
try {
|
||||
html += await this.liquid.renderer.renderTemplates(this.templates, ctx)
|
||||
} catch (e) {
|
||||
if (e.name === 'RenderBreakError') {
|
||||
html += e.resolvedHTML
|
||||
if (e.message === 'break') break
|
||||
} else throw e
|
||||
}
|
||||
context.forloop.next()
|
||||
}
|
||||
ctx.pop()
|
||||
return html
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { evalExp, isTruthy } from '../../render/syntax'
|
||||
import TagToken from '../../parser/tag-token'
|
||||
import Token from '../../parser/token'
|
||||
import Context from '../../context/context'
|
||||
import ITemplate from '../../template/itemplate'
|
||||
import ITagImplOptions from '../../template/tag/itag-impl-options'
|
||||
import ParseStream from '../../parser/parse-stream'
|
||||
|
||||
export default {
|
||||
parse: function (tagToken: TagToken, remainTokens: Token[]) {
|
||||
this.branches = []
|
||||
this.elseTemplates = []
|
||||
|
||||
let p
|
||||
const stream: ParseStream = this.liquid.parser.parseStream(remainTokens)
|
||||
.on('start', () => this.branches.push({
|
||||
cond: tagToken.args,
|
||||
templates: (p = [])
|
||||
}))
|
||||
.on('tag:elsif', (token: TagToken) => {
|
||||
this.branches.push({
|
||||
cond: token.args,
|
||||
templates: p = []
|
||||
})
|
||||
})
|
||||
.on('tag:else', () => (p = this.elseTemplates))
|
||||
.on('tag:endif', () => stream.stop())
|
||||
.on('template', (tpl: ITemplate) => p.push(tpl))
|
||||
.on('end', () => {
|
||||
throw new Error(`tag ${tagToken.raw} not closed`)
|
||||
})
|
||||
|
||||
stream.start()
|
||||
},
|
||||
|
||||
render: async function (ctx: Context) {
|
||||
for (const branch of this.branches) {
|
||||
const cond = await evalExp(branch.cond, ctx)
|
||||
if (isTruthy(cond)) {
|
||||
return this.liquid.renderer.renderTemplates(branch.templates, ctx)
|
||||
}
|
||||
}
|
||||
return this.liquid.renderer.renderTemplates(this.elseTemplates, ctx)
|
||||
}
|
||||
} as ITagImplOptions
|
||||
@@ -0,0 +1,54 @@
|
||||
import assert from '../../util/assert'
|
||||
import { value, quotedLine } from '../../parser/lexical'
|
||||
import { evalValue, parseValue } from '../../render/syntax'
|
||||
import BlockMode from '../../context/block-mode'
|
||||
import TagToken from '../../parser/tag-token'
|
||||
import Context from '../../context/context'
|
||||
import Hash from '../../template/tag/hash'
|
||||
import ITagImplOptions from '../../template/tag/itag-impl-options'
|
||||
|
||||
const staticFileRE = /[^\s,]+/
|
||||
const withRE = new RegExp(`with\\s+(${value.source})`)
|
||||
|
||||
export default <ITagImplOptions>{
|
||||
parse: function (token: TagToken) {
|
||||
let match = staticFileRE.exec(token.args)
|
||||
if (match) this.staticValue = match[0]
|
||||
|
||||
match = value.exec(token.args)
|
||||
if (match) this.value = match[0]
|
||||
|
||||
match = withRE.exec(token.args)
|
||||
if (match) this.with = match[1]
|
||||
},
|
||||
render: async function (ctx: Context, hash: Hash) {
|
||||
let filepath
|
||||
if (ctx.opts.dynamicPartials) {
|
||||
if (quotedLine.exec(this.value)) {
|
||||
const template = this.value.slice(1, -1)
|
||||
filepath = await this.liquid.parseAndRender(template, ctx.getAll(), ctx.opts)
|
||||
} else {
|
||||
filepath = await evalValue(this.value, ctx)
|
||||
}
|
||||
} else {
|
||||
filepath = this.staticValue
|
||||
}
|
||||
assert(filepath, `cannot include with empty filename`)
|
||||
|
||||
const originBlocks = ctx.getRegister('blocks')
|
||||
const originBlockMode = ctx.getRegister('blockMode')
|
||||
|
||||
ctx.setRegister('blocks', {})
|
||||
ctx.setRegister('blockMode', BlockMode.OUTPUT)
|
||||
if (this.with) {
|
||||
hash[filepath] = await parseValue(this.with, ctx)
|
||||
}
|
||||
const templates = await this.liquid.getTemplate(filepath, ctx.opts)
|
||||
ctx.push(hash)
|
||||
const html = await this.liquid.renderer.renderTemplates(templates, ctx)
|
||||
ctx.pop()
|
||||
ctx.setRegister('blocks', originBlocks)
|
||||
ctx.setRegister('blockMode', originBlockMode)
|
||||
return html
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import assert from '../../util/assert'
|
||||
import { identifier } from '../../parser/lexical'
|
||||
import { isNumber } from '../../util/underscore'
|
||||
import ITagImplOptions from '../../template/tag/itag-impl-options'
|
||||
|
||||
export default {
|
||||
parse: function (token) {
|
||||
const match = token.args.match(identifier)
|
||||
assert(match, `illegal identifier ${token.args}`)
|
||||
this.variable = match![0]
|
||||
},
|
||||
render: function (context) {
|
||||
const scope = context.environments
|
||||
if (!isNumber(scope[this.variable])) {
|
||||
scope[this.variable] = 0
|
||||
}
|
||||
const val = scope[this.variable]
|
||||
scope[this.variable]++
|
||||
return val
|
||||
}
|
||||
} as ITagImplOptions
|
||||
@@ -0,0 +1,24 @@
|
||||
import assign from './assign'
|
||||
import For from './for'
|
||||
import capture from './capture'
|
||||
import Case from './case'
|
||||
import comment from './comment'
|
||||
import include from './include'
|
||||
import decrement from './decrement'
|
||||
import cycle from './cycle'
|
||||
import If from './if'
|
||||
import increment from './increment'
|
||||
import layout from './layout'
|
||||
import block from './block'
|
||||
import raw from './raw'
|
||||
import tablerow from './tablerow'
|
||||
import unless from './unless'
|
||||
import Break from './break'
|
||||
import Continue from './continue'
|
||||
import ITagImplOptions from '../../template/tag/itag-impl-options'
|
||||
|
||||
const tags: { [key: string]: ITagImplOptions } = {
|
||||
assign, 'for': For, capture, 'case': Case, comment, include, decrement, increment, cycle, 'if': If, layout, block, raw, tablerow, unless, 'break': Break, 'continue': Continue
|
||||
}
|
||||
|
||||
export default tags
|
||||
@@ -0,0 +1,47 @@
|
||||
import assert from '../../util/assert'
|
||||
import { value as rValue } from '../../parser/lexical'
|
||||
import { evalValue } from '../../render/syntax'
|
||||
import BlockMode from '../../context/block-mode'
|
||||
import TagToken from '../../parser/tag-token'
|
||||
import Token from '../../parser/token'
|
||||
import Context from '../../context/context'
|
||||
import Hash from '../../template/tag/hash'
|
||||
import ITagImplOptions from '../../template/tag/itag-impl-options'
|
||||
|
||||
const staticFileRE = /\S+/
|
||||
|
||||
export default {
|
||||
parse: function (token: TagToken, remainTokens: Token[]) {
|
||||
let match = staticFileRE.exec(token.args)
|
||||
if (match) {
|
||||
this.staticLayout = match[0]
|
||||
}
|
||||
|
||||
match = rValue.exec(token.args)
|
||||
if (match) {
|
||||
this.layout = match[0]
|
||||
}
|
||||
|
||||
this.tpls = this.liquid.parser.parse(remainTokens)
|
||||
},
|
||||
render: async function (ctx: Context, hash: Hash) {
|
||||
const layout = ctx.opts.dynamicPartials
|
||||
? await evalValue(this.layout, ctx)
|
||||
: this.staticLayout
|
||||
assert(layout, `cannot apply layout with empty filename`)
|
||||
|
||||
// render the remaining tokens immediately
|
||||
ctx.setRegister('blockMode', BlockMode.STORE)
|
||||
const blocks = ctx.getRegister('blocks')
|
||||
const html = await this.liquid.renderer.renderTemplates(this.tpls, ctx)
|
||||
if (blocks[''] === undefined) {
|
||||
blocks[''] = html
|
||||
}
|
||||
const templates = await this.liquid.getTemplate(layout, ctx.opts)
|
||||
ctx.push(hash)
|
||||
ctx.setRegister('blockMode', BlockMode.OUTPUT)
|
||||
const partial = await this.liquid.renderer.renderTemplates(templates, ctx)
|
||||
ctx.pop()
|
||||
return partial
|
||||
}
|
||||
} as ITagImplOptions
|
||||
@@ -0,0 +1,23 @@
|
||||
import TagToken from '../../parser/tag-token'
|
||||
import Token from '../../parser/token'
|
||||
import ITagImplOptions from '../../template/tag/itag-impl-options'
|
||||
|
||||
export default <ITagImplOptions>{
|
||||
parse: function (tagToken: TagToken, remainTokens: Token[]) {
|
||||
this.tokens = []
|
||||
|
||||
const stream = this.liquid.parser.parseStream(remainTokens)
|
||||
stream
|
||||
.on('token', (token: TagToken) => {
|
||||
if (token.name === 'endraw') stream.stop()
|
||||
else this.tokens.push(token)
|
||||
})
|
||||
.on('end', () => {
|
||||
throw new Error(`tag ${tagToken.raw} not closed`)
|
||||
})
|
||||
stream.start()
|
||||
},
|
||||
render: function () {
|
||||
return this.tokens.map((token: Token) => token.raw).join('')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import assert from '../../util/assert'
|
||||
import { evalExp } from '../../render/syntax'
|
||||
import { identifier, value, hash } from '../../parser/lexical'
|
||||
import TagToken from '../../parser/tag-token'
|
||||
import Token from '../../parser/token'
|
||||
import ITemplate from '../../template/itemplate'
|
||||
import Context from '../../context/context'
|
||||
import Hash from '../../template/tag/hash'
|
||||
import ITagImplOptions from '../../template/tag/itag-impl-options'
|
||||
import ParseStream from '../../parser/parse-stream'
|
||||
import { TablerowloopDrop } from '../../drop/tablerowloop-drop'
|
||||
|
||||
const re = new RegExp(`^(${identifier.source})\\s+in\\s+` +
|
||||
`(${value.source})` +
|
||||
`(?:\\s+${hash.source})*$`)
|
||||
|
||||
export default {
|
||||
parse: function (tagToken: TagToken, remainTokens: Token[]) {
|
||||
const match = re.exec(tagToken.args) as RegExpExecArray
|
||||
assert(match, `illegal tag: ${tagToken.raw}`)
|
||||
|
||||
this.variable = match[1]
|
||||
this.collection = match[2]
|
||||
this.templates = []
|
||||
|
||||
let p
|
||||
const stream: ParseStream = this.liquid.parser.parseStream(remainTokens)
|
||||
.on('start', () => (p = this.templates))
|
||||
.on('tag:endtablerow', () => stream.stop())
|
||||
.on('template', (tpl: ITemplate) => p.push(tpl))
|
||||
.on('end', () => {
|
||||
throw new Error(`tag ${tagToken.raw} not closed`)
|
||||
})
|
||||
|
||||
stream.start()
|
||||
},
|
||||
|
||||
render: async function (ctx: Context, hash: Hash) {
|
||||
let collection = await evalExp(this.collection, ctx) || []
|
||||
const offset = hash.offset || 0
|
||||
const limit = (hash.limit === undefined) ? collection.length : hash.limit
|
||||
|
||||
collection = collection.slice(offset, offset + limit)
|
||||
const cols = hash.cols || collection.length
|
||||
|
||||
const tablerowloop = new TablerowloopDrop(collection.length, cols)
|
||||
const scope = { tablerowloop }
|
||||
ctx.push(scope)
|
||||
|
||||
let html = ''
|
||||
for (let idx = 0; idx < collection.length; idx++, tablerowloop.next()) {
|
||||
scope[this.variable] = collection[idx]
|
||||
if (tablerowloop.col0() === 0) {
|
||||
if (tablerowloop.row() !== 1) html += '</tr>'
|
||||
html += `<tr class="row${tablerowloop.row()}">`
|
||||
}
|
||||
html += `<td class="col${tablerowloop.col()}">`
|
||||
html += await this.liquid.renderer.renderTemplates(this.templates, ctx)
|
||||
html += '</td>'
|
||||
}
|
||||
if (collection.length) html += '</tr>'
|
||||
ctx.pop()
|
||||
return html
|
||||
}
|
||||
} as ITagImplOptions
|
||||
@@ -0,0 +1,34 @@
|
||||
import { evalExp, isFalsy } from '../../render/syntax'
|
||||
import TagToken from '../../parser/tag-token'
|
||||
import Token from '../../parser/token'
|
||||
import Context from '../../context/context'
|
||||
import ITagImplOptions from '../../template/tag/itag-impl-options'
|
||||
import ParseStream from '../../parser/parse-stream'
|
||||
|
||||
export default {
|
||||
parse: function (tagToken: TagToken, remainTokens: Token[]) {
|
||||
this.templates = []
|
||||
this.elseTemplates = []
|
||||
let p
|
||||
const stream: ParseStream = this.liquid.parser.parseStream(remainTokens)
|
||||
.on('start', () => {
|
||||
p = this.templates
|
||||
this.cond = tagToken.args
|
||||
})
|
||||
.on('tag:else', () => (p = this.elseTemplates))
|
||||
.on('tag:endunless', () => stream.stop())
|
||||
.on('template', tpl => p.push(tpl))
|
||||
.on('end', () => {
|
||||
throw new Error(`tag ${tagToken.raw} not closed`)
|
||||
})
|
||||
|
||||
stream.start()
|
||||
},
|
||||
|
||||
render: async function (ctx: Context) {
|
||||
const cond = await evalExp(this.cond, ctx)
|
||||
return isFalsy(cond)
|
||||
? this.liquid.renderer.renderTemplates(this.templates, ctx)
|
||||
: this.liquid.renderer.renderTemplates(this.elseTemplates, ctx)
|
||||
}
|
||||
} as ITagImplOptions
|
||||
@@ -0,0 +1,8 @@
|
||||
enum BlockMode {
|
||||
/* store rendered html into blocks */
|
||||
OUTPUT,
|
||||
/* output rendered html directly */
|
||||
STORE
|
||||
}
|
||||
|
||||
export default BlockMode
|
||||
@@ -0,0 +1,147 @@
|
||||
import * as _ from '../util/underscore'
|
||||
import { Drop } from '../drop/drop'
|
||||
import { __assign } from 'tslib'
|
||||
import assert from '../util/assert'
|
||||
import { NormalizedFullOptions, applyDefault } from '../liquid-options'
|
||||
import { Scope } from './scope'
|
||||
|
||||
export default class Context {
|
||||
opts: NormalizedFullOptions
|
||||
environments: Scope
|
||||
private scopes: Array<Scope> = [{}]
|
||||
private registers = {}
|
||||
constructor (ctx: object = {}, opts?: NormalizedFullOptions) {
|
||||
this.opts = applyDefault(opts)
|
||||
this.environments = ctx
|
||||
}
|
||||
getRegister (key: string, defaultValue = {}) {
|
||||
return (this.registers[key] = this.registers[key] || defaultValue)
|
||||
}
|
||||
setRegister (key: string, value: any) {
|
||||
return (this.registers[key] = value)
|
||||
}
|
||||
getAll () {
|
||||
return [this.environments, ...this.scopes]
|
||||
.reduce((ctx, val) => __assign(ctx, val), {})
|
||||
}
|
||||
async get (path: string) {
|
||||
const paths = await this.parseProp(path)
|
||||
let ctx = this.findScope(paths[0]) || this.environments
|
||||
for (const path of paths) {
|
||||
ctx = readProperty(ctx, path)
|
||||
if (_.isNil(ctx) && this.opts.strictVariables) {
|
||||
throw new TypeError(`undefined variable: ${path}`)
|
||||
}
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
push (ctx: object) {
|
||||
return this.scopes.push(ctx)
|
||||
}
|
||||
pop () {
|
||||
return this.scopes.pop()
|
||||
}
|
||||
front () {
|
||||
return this.scopes[0]
|
||||
}
|
||||
private findScope (key: string) {
|
||||
for (let i = this.scopes.length - 1; i >= 0; i--) {
|
||||
const candidate = this.scopes[i]
|
||||
if (key in candidate) {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/*
|
||||
* Parse property access sequence from access string
|
||||
* @example
|
||||
* accessSeq("foo.bar") // ['foo', 'bar']
|
||||
* accessSeq("foo['bar']") // ['foo', 'bar']
|
||||
* accessSeq("foo['b]r']") // ['foo', 'b]r']
|
||||
* accessSeq("foo[bar.coo]") // ['foo', 'bar'], for bar.coo == 'bar'
|
||||
*/
|
||||
private async parseProp (str: string) {
|
||||
str = String(str)
|
||||
const seq: string[] = []
|
||||
let name = ''
|
||||
let j
|
||||
let i = 0
|
||||
while (i < str.length) {
|
||||
switch (str[i]) {
|
||||
case '[':
|
||||
push()
|
||||
|
||||
const delemiter = str[i + 1]
|
||||
if (/['"]/.test(delemiter)) { // foo["bar"]
|
||||
j = str.indexOf(delemiter, i + 2)
|
||||
assert(j !== -1, `unbalanced ${delemiter}: ${str}`)
|
||||
name = str.slice(i + 2, j)
|
||||
push()
|
||||
i = j + 2
|
||||
} else { // foo[bar.coo]
|
||||
j = matchRightBracket(str, i + 1)
|
||||
assert(j !== -1, `unbalanced []: ${str}`)
|
||||
name = str.slice(i + 1, j)
|
||||
if (!/^[+-]?\d+$/.test(name)) { // foo[bar] vs. foo[1]
|
||||
name = String(await this.get(name))
|
||||
}
|
||||
push()
|
||||
i = j + 1
|
||||
}
|
||||
break
|
||||
case '.':// foo.bar, foo[0].bar
|
||||
push()
|
||||
i++
|
||||
break
|
||||
default:// foo.bar
|
||||
name += str[i++]
|
||||
}
|
||||
}
|
||||
push()
|
||||
|
||||
if (!seq.length) {
|
||||
throw new TypeError(`invalid path:"${str}"`)
|
||||
}
|
||||
return seq
|
||||
|
||||
function push () {
|
||||
if (name.length) seq.push(name)
|
||||
name = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function readProperty (obj: Scope, key: string) {
|
||||
if (_.isNil(obj)) return obj
|
||||
obj = _.toLiquid(obj)
|
||||
if (obj instanceof Drop) {
|
||||
if (_.isFunction(obj[key])) return obj[key]()
|
||||
if (obj.hasOwnProperty(key)) return obj[key]
|
||||
return obj.liquidMethodMissing(key)
|
||||
}
|
||||
return key === 'size' ? readSize(obj) : obj[key]
|
||||
}
|
||||
|
||||
function readSize (obj: Scope) {
|
||||
if (!_.isNil(obj['size'])) return obj['size']
|
||||
if (_.isArray(obj) || _.isString(obj)) return obj.length
|
||||
return obj['size']
|
||||
}
|
||||
|
||||
function matchRightBracket (str: string, begin: number) {
|
||||
let stack = 1 // count of '[' - count of ']'
|
||||
for (let i = begin; i < str.length; i++) {
|
||||
if (str[i] === '[') {
|
||||
stack++
|
||||
}
|
||||
if (str[i] === ']') {
|
||||
stack--
|
||||
if (stack === 0) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Drop } from '../drop/drop'
|
||||
|
||||
type PlainObject = {
|
||||
[key: string]: any
|
||||
toLiquid?: () => any
|
||||
}
|
||||
|
||||
export type Scope = PlainObject | Drop
|
||||
@@ -0,0 +1,11 @@
|
||||
import { isNil, isString, toValue } from '../util/underscore'
|
||||
import { EmptyDrop } from '../drop/empty-drop'
|
||||
|
||||
export class BlankDrop extends EmptyDrop {
|
||||
equals (value: any) {
|
||||
if (value === false) return true
|
||||
if (isNil(toValue(value))) return true
|
||||
if (isString(value)) return /^\s*$/.test(value)
|
||||
return super.equals(value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export abstract class Drop {
|
||||
valueOf (): any {
|
||||
return undefined
|
||||
}
|
||||
|
||||
liquidMethodMissing (key: string): Promise<string | undefined> | string | undefined {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Drop } from './drop'
|
||||
import { IComparable } from './icomparable'
|
||||
import { isObject, isString, isArray } from '../util/underscore'
|
||||
|
||||
export class EmptyDrop extends Drop implements IComparable {
|
||||
equals (value: any) {
|
||||
if (isString(value) || isArray(value)) return value.length === 0
|
||||
if (isObject(value)) return Object.keys(value).length === 0
|
||||
return false
|
||||
}
|
||||
gt () {
|
||||
return false
|
||||
}
|
||||
geq () {
|
||||
return false
|
||||
}
|
||||
lt () {
|
||||
return false
|
||||
}
|
||||
leq () {
|
||||
return false
|
||||
}
|
||||
valueOf () {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Drop } from './drop'
|
||||
|
||||
export class ForloopDrop extends Drop {
|
||||
protected i: number = 0
|
||||
length: number
|
||||
constructor (length: number) {
|
||||
super()
|
||||
this.length = length
|
||||
}
|
||||
next () {
|
||||
this.i++
|
||||
}
|
||||
index0 () {
|
||||
return this.i
|
||||
}
|
||||
index () {
|
||||
return this.i + 1
|
||||
}
|
||||
first () {
|
||||
return this.i === 0
|
||||
}
|
||||
last () {
|
||||
return this.i === this.length - 1
|
||||
}
|
||||
rindex () {
|
||||
return this.length - this.i
|
||||
}
|
||||
rindex0 () {
|
||||
return this.length - this.i - 1
|
||||
}
|
||||
valueOf () {
|
||||
return JSON.stringify(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { isFunction } from '../util/underscore'
|
||||
|
||||
export interface IComparable {
|
||||
equals: (rhs: any) => boolean
|
||||
gt: (rhs: any) => boolean
|
||||
geq: (rhs: any) => boolean
|
||||
lt: (rhs: any) => boolean
|
||||
leq: (rhs: any) => boolean
|
||||
}
|
||||
|
||||
export function isComparable (arg: any): arg is IComparable {
|
||||
return arg && isFunction(arg.equals)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Drop } from './drop'
|
||||
import { IComparable } from './icomparable'
|
||||
import { isNil, toValue } from '../util/underscore'
|
||||
import { BlankDrop } from '../drop/blank-drop'
|
||||
|
||||
export class NullDrop extends Drop implements IComparable {
|
||||
equals (value: any) {
|
||||
return isNil(toValue(value)) || value instanceof BlankDrop
|
||||
}
|
||||
gt () {
|
||||
return false
|
||||
}
|
||||
geq () {
|
||||
return false
|
||||
}
|
||||
lt () {
|
||||
return false
|
||||
}
|
||||
leq () {
|
||||
return false
|
||||
}
|
||||
valueOf () {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ForloopDrop } from './forloop-drop'
|
||||
|
||||
export class TablerowloopDrop extends ForloopDrop {
|
||||
private cols: number
|
||||
constructor (length: number, cols: number) {
|
||||
super(length)
|
||||
this.length = length
|
||||
this.cols = cols
|
||||
}
|
||||
row () {
|
||||
return Math.floor(this.i / this.cols) + 1
|
||||
}
|
||||
col0 () {
|
||||
return (this.i % this.cols)
|
||||
}
|
||||
col () {
|
||||
return this.col0() + 1
|
||||
}
|
||||
col_first () { // eslint-disable-line
|
||||
return this.col0() === 0
|
||||
}
|
||||
col_last () { // eslint-disable-line
|
||||
return this.col() === this.cols
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import * as lexical from './lexical.js'
|
||||
import {evalValue} from './syntax.js'
|
||||
import assert from './util/assert.js'
|
||||
import {assign, create} from './util/underscore.js'
|
||||
|
||||
const valueRE = new RegExp(`${lexical.value.source}`, 'g')
|
||||
|
||||
export default function (options) {
|
||||
options = assign({}, options)
|
||||
let filters = {}
|
||||
|
||||
const _filterInstance = {
|
||||
render: function (output, scope) {
|
||||
const args = this.args.map(arg => evalValue(arg, scope))
|
||||
args.unshift(output)
|
||||
return this.filter.apply(null, args)
|
||||
},
|
||||
parse: function (str) {
|
||||
let match = lexical.filterLine.exec(str)
|
||||
assert(match, 'illegal filter: ' + str)
|
||||
|
||||
const name = match[1]
|
||||
const argList = match[2] || ''
|
||||
const filter = filters[name]
|
||||
if (typeof filter !== 'function') {
|
||||
if (options.strict_filters) {
|
||||
throw new TypeError(`undefined filter: ${name}`)
|
||||
}
|
||||
this.name = name
|
||||
this.filter = x => x
|
||||
this.args = []
|
||||
return this
|
||||
}
|
||||
|
||||
const args = []
|
||||
while ((match = valueRE.exec(argList.trim()))) {
|
||||
const v = match[0]
|
||||
const re = new RegExp(`${v}\\s*:`, 'g')
|
||||
const keyMatch = re.exec(match.input)
|
||||
const currentMatchIsKey = keyMatch && keyMatch.index === match.index
|
||||
currentMatchIsKey ? args.push(`'${v}'`) : args.push(v)
|
||||
}
|
||||
|
||||
this.name = name
|
||||
this.filter = filter
|
||||
this.args = args
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
function construct (str) {
|
||||
const instance = create(_filterInstance)
|
||||
return instance.parse(str)
|
||||
}
|
||||
|
||||
function register (name, filter) {
|
||||
filters[name] = filter
|
||||
}
|
||||
|
||||
function clear () {
|
||||
filters = {}
|
||||
}
|
||||
|
||||
return {
|
||||
construct, register, clear
|
||||
}
|
||||
}
|
||||
-139
@@ -1,139 +0,0 @@
|
||||
import strftime from './util/strftime.js'
|
||||
import * as _ from './util/underscore.js'
|
||||
import {isTruthy} from './syntax.js'
|
||||
|
||||
const escapeMap = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
}
|
||||
const unescapeMap = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
''': "'"
|
||||
}
|
||||
|
||||
const filters = {
|
||||
'abs': v => Math.abs(v),
|
||||
'append': (v, arg) => v + arg,
|
||||
'capitalize': str => stringify(str).charAt(0).toUpperCase() + str.slice(1),
|
||||
'ceil': v => Math.ceil(v),
|
||||
'concat': (v, arg) => Array.prototype.concat.call(v, arg),
|
||||
'date': (v, arg) => {
|
||||
let date = v
|
||||
if (v === 'now') {
|
||||
date = new Date()
|
||||
} else if (_.isString(v)) {
|
||||
date = new Date(v)
|
||||
}
|
||||
return isValidDate(date) ? strftime(date, arg) : v
|
||||
},
|
||||
'default': (v, arg) => isTruthy(v) ? v : arg,
|
||||
'divided_by': (v, arg) => v / arg,
|
||||
'downcase': v => v.toLowerCase(),
|
||||
'escape': escape,
|
||||
|
||||
'escape_once': str => escape(unescape(str)),
|
||||
'first': v => v[0],
|
||||
'floor': v => Math.floor(v),
|
||||
'join': (v, arg) => v.join(arg === undefined ? ' ' : arg),
|
||||
'last': v => _.last(v),
|
||||
'lstrip': v => stringify(v).replace(/^\s+/, ''),
|
||||
'map': (arr, arg) => arr.map(v => v[arg]),
|
||||
'minus': bindFixed((v, arg) => v - arg),
|
||||
'modulo': bindFixed((v, arg) => v % arg),
|
||||
'newline_to_br': v => v.replace(/\n/g, '<br />'),
|
||||
'plus': bindFixed((v, arg) => Number(v) + Number(arg)),
|
||||
'prepend': (v, arg) => arg + v,
|
||||
'remove': (v, arg) => v.split(arg).join(''),
|
||||
'remove_first': (v, l) => v.replace(l, ''),
|
||||
'replace': (v, pattern, replacement) =>
|
||||
stringify(v).split(pattern).join(replacement),
|
||||
'replace_first': (v, arg1, arg2) => stringify(v).replace(arg1, arg2),
|
||||
'reverse': v => v.reverse(),
|
||||
'round': (v, arg) => {
|
||||
const amp = Math.pow(10, arg || 0)
|
||||
return Math.round(v * amp, arg) / amp
|
||||
},
|
||||
'rstrip': str => stringify(str).replace(/\s+$/, ''),
|
||||
'size': v => v.length,
|
||||
'slice': (v, begin, length) => {
|
||||
if (length === undefined) length = 1
|
||||
return v.slice(begin, begin + length)
|
||||
},
|
||||
'sort': (v, arg) => v.sort(arg),
|
||||
'split': (v, arg) => stringify(v).split(arg),
|
||||
'strip': (v) => stringify(v).trim(),
|
||||
'strip_html': v => stringify(v).replace(/<script.*?<\/script>|<!--.*?-->|<style.*?<\/style>|<.*?>/g, ''),
|
||||
'strip_newlines': v => stringify(v).replace(/\n/g, ''),
|
||||
'times': (v, arg) => v * arg,
|
||||
'truncate': (v, l, o) => {
|
||||
v = stringify(v)
|
||||
o = (o === undefined) ? '...' : o
|
||||
l = l || 16
|
||||
if (v.length <= l) return v
|
||||
return v.substr(0, l - o.length) + o
|
||||
},
|
||||
'truncatewords': (v, l, o) => {
|
||||
if (o === undefined) o = '...'
|
||||
const arr = v.split(' ')
|
||||
let ret = arr.slice(0, l).join(' ')
|
||||
if (arr.length > l) ret += o
|
||||
return ret
|
||||
},
|
||||
'uniq': function (arr) {
|
||||
const u = {}
|
||||
return (arr || []).filter(val => {
|
||||
if (u.hasOwnProperty(val)) {
|
||||
return false
|
||||
}
|
||||
u[val] = true
|
||||
return true
|
||||
})
|
||||
},
|
||||
'upcase': str => stringify(str).toUpperCase(),
|
||||
'url_decode': x => x.split('+').map(decodeURIComponent).join(' '),
|
||||
'url_encode': x => x.split(' ').map(encodeURIComponent).join('+')
|
||||
}
|
||||
|
||||
function escape (str) {
|
||||
return stringify(str).replace(/&|<|>|"|'/g, m => escapeMap[m])
|
||||
}
|
||||
|
||||
function unescape (str) {
|
||||
return stringify(str).replace(/&(amp|lt|gt|#34|#39);/g, m => unescapeMap[m])
|
||||
}
|
||||
|
||||
function getFixed (v) {
|
||||
const p = (v + '').split('.')
|
||||
return (p.length > 1) ? p[1].length : 0
|
||||
}
|
||||
|
||||
function getMaxFixed (l, r) {
|
||||
return Math.max(getFixed(l), getFixed(r))
|
||||
}
|
||||
|
||||
function stringify (obj) {
|
||||
return obj + ''
|
||||
}
|
||||
|
||||
function bindFixed (cb) {
|
||||
return (l, r) => {
|
||||
const f = getMaxFixed(l, r)
|
||||
return cb(l, r).toFixed(f)
|
||||
}
|
||||
}
|
||||
|
||||
function isValidDate (date) {
|
||||
return date instanceof Date && !isNaN(date.getTime())
|
||||
}
|
||||
|
||||
export default function registerAll (liquid) {
|
||||
return _.forOwn(filters, (func, name) => liquid.registerFilter(name, func))
|
||||
}
|
||||
|
||||
registerAll.filters = filters
|
||||
@@ -1,6 +1,7 @@
|
||||
import {last, isArray} from './util/underscore'
|
||||
import { last } from '../util/underscore'
|
||||
import IFS from './ifs'
|
||||
|
||||
function domResolve (root, path) {
|
||||
function domResolve (root: string, path: string) {
|
||||
const base = document.createElement('base')
|
||||
base.href = root
|
||||
|
||||
@@ -15,30 +16,22 @@ function domResolve (root, path) {
|
||||
return resolved
|
||||
}
|
||||
|
||||
export function resolve (filepath, root, options) {
|
||||
root = root || options.root
|
||||
if (isArray(root)) {
|
||||
root = root[0]
|
||||
}
|
||||
if (root.length && last(root) !== '/') {
|
||||
root += '/'
|
||||
}
|
||||
function resolve (root: string, filepath: string, ext: string) {
|
||||
if (root.length && last(root) !== '/') root += '/'
|
||||
const url = domResolve(root, filepath)
|
||||
return url.replace(/^(\w+:\/\/[^/]+)(\/[^?]+)/, (str, origin, path) => {
|
||||
const last = path.split('/').pop()
|
||||
if (/\.\w+$/.test(last)) {
|
||||
return str
|
||||
}
|
||||
return origin + path + options.extname
|
||||
if (/\.\w+$/.test(last)) return str
|
||||
return origin + path + ext
|
||||
})
|
||||
}
|
||||
|
||||
export async function read (url) {
|
||||
async function readFile (url: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.onload = () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
resolve(xhr.responseText)
|
||||
resolve(xhr.responseText as string)
|
||||
} else {
|
||||
reject(new Error(xhr.statusText))
|
||||
}
|
||||
@@ -50,3 +43,9 @@ export async function read (url) {
|
||||
xhr.send()
|
||||
})
|
||||
}
|
||||
|
||||
async function exists () {
|
||||
return true
|
||||
}
|
||||
|
||||
export default { readFile, resolve, exists } as IFS
|
||||
@@ -0,0 +1,5 @@
|
||||
export default interface IFS {
|
||||
exists: (filepath: string) => Promise<boolean>
|
||||
readFile: (filepath:string) => Promise<string>
|
||||
resolve: (root: string, file: string, ext: string) => string
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import * as _ from '../util/underscore'
|
||||
import { resolve, extname } from 'path'
|
||||
import { stat, readFile } from 'fs'
|
||||
import IFS from './ifs'
|
||||
|
||||
const statAsync = _.promisify(stat)
|
||||
const readFileAsync = _.promisify<string, string, string>(readFile)
|
||||
|
||||
const fs: IFS = {
|
||||
exists: (filepath: string) => {
|
||||
return statAsync(filepath).then(() => true).catch(() => false)
|
||||
},
|
||||
readFile: filepath => {
|
||||
return readFileAsync(filepath, 'utf8')
|
||||
},
|
||||
resolve: (root: string, file: string, ext: string) => {
|
||||
if (!extname(file)) file += ext
|
||||
return resolve(root, file)
|
||||
}
|
||||
}
|
||||
|
||||
export default fs
|
||||
Vendored
-81
@@ -1,81 +0,0 @@
|
||||
declare function Liquid(): Liquid.LiquidEngine;
|
||||
|
||||
declare namespace Liquid {
|
||||
export interface LiquidEngine {
|
||||
private init(tag, filter, options): LiquidEngine
|
||||
private respectCache(key, getter): Promise<any>
|
||||
private evalValue: typeof Liquid.evalValue
|
||||
|
||||
public parse(html: string, filepath: string): Liquid.Template
|
||||
public render(tpl: Template, ctx: any, opts: Options): Promise<string>
|
||||
public parseAndRender(html: string, ctx: any, opts: Options): Promise<string>
|
||||
public getTemplate(file: string, root: string): Promise<Liquid.Template>
|
||||
public renderFile(file: string, ctx: any, opts: Options): Promise<string>
|
||||
public registerFilter(name: string, filter: Filter): void
|
||||
public registerTag(name: string, tag: Tag): void
|
||||
public express(opts: Options): any
|
||||
}
|
||||
|
||||
export interface Template { }
|
||||
export interface Options {
|
||||
/** `root` is a directory or an array of directories to resolve layouts and includes, as well as the filename passed in when calling `.renderFile()`. If an array, the files are looked up in the order they occur in the array. Defaults to `["."]`*/
|
||||
root: string | string[]
|
||||
/** `extname` is used to lookup the template file when filepath doesn't include an extension name. Eg: setting to `".html"` will allow including file by basename. Defaults to `""`. */
|
||||
extname: string
|
||||
/** `cache` indicates whether or not to cache resolved templates. Defaults to `false`. */
|
||||
cache: boolean
|
||||
/** `dynamicPartials`: if set, treat `<filepath>` parameter in `{%include filepath %}`, `{%layout filepath%}` as a variable, otherwise as a literal value. Defaults to `true`. */
|
||||
dynamicPartials: boolean
|
||||
/** `strict_filters` is used to enable strict filter existence. If set to `false`, undefined filters will be rendered as empty string. Otherwise, undefined filters will cause an exception. Defaults to `false`. */
|
||||
strict_filters: boolean
|
||||
/** `trim_tag_right` is used to strip blank characters (including ` `, `\t`, and `\r`) from the right of tags (`{% %}`) until `\n` (inclusive). Defaults to `false`. */
|
||||
trim_tag_right: boolean
|
||||
/** `trim_tag_left` is similiar to `trim_tag_right`, whereas the `\n` is exclusive. Defaults to `false`. See Whitespace Control for details. */
|
||||
trim_tag_left: boolean
|
||||
/** ``trim_value_right` is used to strip blank characters (including ` `, `\t`, and `\r`) from the right of values (`{{ }}`) until `\n` (inclusive). Defaults to `false`. */
|
||||
trim_value_right: boolean
|
||||
/** `trim_value_left` is similiar to `trim_value_right`, whereas the `\n` is exclusive. Defaults to `false`. See Whitespace Control for details. */
|
||||
trim_value_left: boolean
|
||||
/** `greedy` is used to specify whether `trim_left`/`trim_right` is greedy. When set to `true`, all consecutive blank characters including `\n` will be trimed regardless of line breaks. Defaults to `true`. */
|
||||
greedy: boolean
|
||||
}
|
||||
|
||||
export interface Tag {
|
||||
// TODO: tokens type; this type
|
||||
parse(this: any, tagToken: any, remainTokens: any): void
|
||||
render(this: any, scope: any, hash: any): void
|
||||
}
|
||||
export type Filter = (...args?: any) => string
|
||||
|
||||
export function isTruthy(val: any): boolean;
|
||||
export function isFalsy(val: any): boolean;
|
||||
export function evalExp(exp: string, scope: any): any;
|
||||
export function evalValue(str: string, scope: any): any;
|
||||
export const Types: Types;
|
||||
|
||||
declare class LiquidError extends Error {
|
||||
public input: string
|
||||
public line: number
|
||||
public file: string
|
||||
}
|
||||
export class ParseError extends LiquidError {
|
||||
public originalError: Error
|
||||
}
|
||||
export class TokenizationError extends LiquidError { }
|
||||
export class RenderBreakError extends LiquidError { }
|
||||
export class AssertionError extends LiquidError { }
|
||||
|
||||
export interface Types {
|
||||
ParseError: typeof ParseError,
|
||||
TokenizationError: typeof TokenizationError,
|
||||
RenderBreakError: typeof RenderBreakError,
|
||||
AssertionError: typeof AssertionError,
|
||||
AssignScope: any,
|
||||
CaptureScope: any,
|
||||
IncrementScope: any,
|
||||
DecrementScope: any
|
||||
}
|
||||
}
|
||||
|
||||
export = Liquid;
|
||||
export default Liquid;
|
||||
-133
@@ -1,133 +0,0 @@
|
||||
import 'regenerator-runtime/runtime'
|
||||
import * as Scope from './scope'
|
||||
import * as template from './template'
|
||||
import * as _ from './util/underscore.js'
|
||||
import assert from './util/assert.js'
|
||||
import * as tokenizer from './tokenizer.js'
|
||||
import Render from './render.js'
|
||||
import Tag from './tag.js'
|
||||
import Filter from './filter.js'
|
||||
import Parser from './parser'
|
||||
import {isTruthy, isFalsy, evalExp, evalValue} from './syntax.js'
|
||||
import {ParseError, TokenizationError, RenderBreakError, AssertionError} from './util/error.js'
|
||||
import tags from './tags/index.js'
|
||||
import filters from './filters.js'
|
||||
|
||||
const _engine = {
|
||||
init: function (tag, filter, options) {
|
||||
if (options.cache) {
|
||||
this.cache = {}
|
||||
}
|
||||
this.options = options
|
||||
this.tag = tag
|
||||
this.filter = filter
|
||||
this.parser = Parser(tag, filter)
|
||||
this.renderer = Render()
|
||||
|
||||
tags(this, Liquid)
|
||||
filters(this, Liquid)
|
||||
|
||||
return this
|
||||
},
|
||||
parse: function (html, filepath) {
|
||||
const tokens = tokenizer.parse(html, filepath, this.options)
|
||||
return this.parser.parse(tokens)
|
||||
},
|
||||
render: function (tpl, ctx, opts) {
|
||||
opts = _.assign({}, this.options, opts)
|
||||
const scope = Scope.factory(ctx, opts)
|
||||
return this.renderer.renderTemplates(tpl, scope)
|
||||
},
|
||||
parseAndRender: async function (html, ctx, opts) {
|
||||
const tpl = await this.parse(html)
|
||||
return this.render(tpl, ctx, opts)
|
||||
},
|
||||
getTemplate: async function (file, root) {
|
||||
const filepath = await template.resolve(file, root, this.options)
|
||||
return this.respectCache(filepath, async () => {
|
||||
const str = await template.read(filepath)
|
||||
return this.parse(str, filepath)
|
||||
})
|
||||
},
|
||||
renderFile: async function (file, ctx, opts) {
|
||||
opts = _.assign({}, opts)
|
||||
const templates = await this.getTemplate(file, opts.root)
|
||||
return this.render(templates, ctx, opts)
|
||||
},
|
||||
respectCache: async function (key, getter) {
|
||||
const cacheEnabled = this.options.cache
|
||||
if (cacheEnabled && this.cache[key]) {
|
||||
return this.cache[key]
|
||||
}
|
||||
const value = await getter()
|
||||
if (cacheEnabled) {
|
||||
this.cache[key] = value
|
||||
}
|
||||
return value
|
||||
},
|
||||
evalValue: function (str, scope) {
|
||||
const tpl = this.parser.parseValue(str.trim())
|
||||
return this.renderer.evalValue(tpl, scope)
|
||||
},
|
||||
registerFilter: function (name, filter) {
|
||||
return this.filter.register(name, filter)
|
||||
},
|
||||
registerTag: function (name, tag) {
|
||||
return this.tag.register(name, tag)
|
||||
},
|
||||
plugin: function (plugin) {
|
||||
return plugin.call(this, Liquid)
|
||||
},
|
||||
express: function (opts) {
|
||||
opts = opts || {}
|
||||
const self = this
|
||||
return function (filePath, ctx, cb) {
|
||||
assert(_.isArray(this.root) || _.isString(this.root),
|
||||
'illegal views root, are you using express.js?')
|
||||
opts.root = this.root
|
||||
self.renderFile(filePath, ctx, opts).then(html => cb(null, html), cb)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeStringArray (value) {
|
||||
if (_.isArray(value)) return value
|
||||
if (_.isString(value)) return [value]
|
||||
throw new TypeError('illegal root: ' + value)
|
||||
}
|
||||
|
||||
export default function Liquid (options) {
|
||||
options = _.assign({
|
||||
root: ['.'],
|
||||
cache: false,
|
||||
extname: '',
|
||||
dynamicPartials: true,
|
||||
trim_tag_right: false,
|
||||
trim_tag_left: false,
|
||||
trim_value_right: false,
|
||||
trim_value_left: false,
|
||||
greedy: true,
|
||||
strict_filters: false,
|
||||
strict_variables: false
|
||||
}, options)
|
||||
options.root = normalizeStringArray(options.root)
|
||||
|
||||
const engine = _.create(_engine)
|
||||
engine.init(Tag(), Filter(options), options)
|
||||
return engine
|
||||
}
|
||||
|
||||
Liquid.isTruthy = isTruthy
|
||||
Liquid.isFalsy = isFalsy
|
||||
Liquid.evalExp = evalExp
|
||||
Liquid.evalValue = evalValue
|
||||
Liquid.Types = {
|
||||
ParseError,
|
||||
TokenizationError,
|
||||
RenderBreakError,
|
||||
AssertionError,
|
||||
AssignScope: {},
|
||||
CaptureScope: {},
|
||||
IncrementScope: {},
|
||||
DecrementScope: {}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import * as _ from './util/underscore'
|
||||
import IFS from './fs/ifs'
|
||||
|
||||
export interface LiquidOptions {
|
||||
/** `root` is a directory or an array of directories to resolve layouts and includes, as well as the filename passed in when calling `.renderFile()`. If an array, the files are looked up in the order they occur in the array. Defaults to `["."]` */
|
||||
root?: string | string[]
|
||||
/** `extname` is used to lookup the template file when filepath doesn't include an extension name. Eg: setting to `".html"` will allow including file by basename. Defaults to `""`. */
|
||||
extname?: string
|
||||
/** `cache` indicates whether or not to cache resolved templates. Defaults to `false`. */
|
||||
cache?: boolean
|
||||
/** `dynamicPartials`: if set, treat `<filepath>` parameter in `{%include filepath %}`, `{%layout filepath%}` as a variable, otherwise as a literal value. Defaults to `true`. */
|
||||
dynamicPartials?: boolean
|
||||
/** `strictFilters` is used to enable strict filter existence. If set to `false`, undefined filters will be rendered as empty string. Otherwise, undefined filters will cause an exception. Defaults to `false`. */
|
||||
strictFilters?: boolean
|
||||
/** `strictVariables` is used to enable strict variable derivation. If set to `false`, undefined variables will be rendered as empty string. Otherwise, undefined variables will cause an exception. Defaults to `false`. */
|
||||
strictVariables?: boolean
|
||||
/** `trimTagRight` is used to strip blank characters (including ` `, `\t`, and `\r`) from the right of tags (`{% %}`) until `\n` (inclusive). Defaults to `false`. */
|
||||
trimTagRight?: boolean
|
||||
/** `trimTagLeft` is similar to `trimTagRight`, whereas the `\n` is exclusive. Defaults to `false`. See Whitespace Control for details. */
|
||||
trimTagLeft?: boolean
|
||||
/** ``trimOutputRight` is used to strip blank characters (including ` `, `\t`, and `\r`) from the right of values (`{{ }}`) until `\n` (inclusive). Defaults to `false`. */
|
||||
trimOutputRight?: boolean
|
||||
/** `trimOutputLeft` is similar to `trimOutputRight`, whereas the `\n` is exclusive. Defaults to `false`. See Whitespace Control for details. */
|
||||
trimOutputLeft?: boolean
|
||||
/** `tagDelimiterLeft` and `tagDelimiterRight` are used to override the delimiter for liquid tags **/
|
||||
tagDelimiterLeft?: string,
|
||||
tagDelimiterRight?: string,
|
||||
/** `outputDelimiterLeft` and `outputDelimiterRight` are used to override the delimiter for liquid outputs **/
|
||||
outputDelimiterLeft?: string,
|
||||
outputDelimiterRight?: string,
|
||||
/** `greedy` is used to specify whether `trim*Left`/`trim*Right` is greedy. When set to `true`, all consecutive blank characters including `\n` will be trimed regardless of line breaks. Defaults to `true`. */
|
||||
greedy?: boolean,
|
||||
/** `fs` is used to override the default file-system module with a custom implementation */
|
||||
fs?: IFS
|
||||
}
|
||||
|
||||
interface NormalizedOptions extends LiquidOptions {
|
||||
root?: string[]
|
||||
}
|
||||
|
||||
export interface NormalizedFullOptions extends NormalizedOptions {
|
||||
root: string[]
|
||||
extname: string
|
||||
cache: boolean
|
||||
dynamicPartials: boolean
|
||||
strictFilters: boolean
|
||||
strictVariables: boolean
|
||||
trimTagRight: boolean
|
||||
trimTagLeft: boolean
|
||||
trimOutputRight: boolean
|
||||
trimOutputLeft: boolean
|
||||
tagDelimiterLeft: string,
|
||||
tagDelimiterRight: string,
|
||||
outputDelimiterLeft: string,
|
||||
outputDelimiterRight: string,
|
||||
greedy: boolean
|
||||
}
|
||||
|
||||
const defaultOptions: NormalizedFullOptions = {
|
||||
root: ['.'],
|
||||
cache: false,
|
||||
extname: '',
|
||||
dynamicPartials: true,
|
||||
trimTagRight: false,
|
||||
trimTagLeft: false,
|
||||
trimOutputRight: false,
|
||||
trimOutputLeft: false,
|
||||
greedy: true,
|
||||
tagDelimiterLeft: '{%',
|
||||
tagDelimiterRight: '%}',
|
||||
outputDelimiterLeft: '{{',
|
||||
outputDelimiterRight: '}}',
|
||||
strictFilters: false,
|
||||
strictVariables: false
|
||||
}
|
||||
|
||||
export function normalize (options?: LiquidOptions): NormalizedOptions {
|
||||
options = options || {}
|
||||
if (options.hasOwnProperty('root')) {
|
||||
options.root = normalizeStringArray(options.root)
|
||||
}
|
||||
return options as NormalizedOptions
|
||||
}
|
||||
|
||||
export function applyDefault (options?: NormalizedOptions): NormalizedFullOptions {
|
||||
return { ...defaultOptions, ...options }
|
||||
}
|
||||
|
||||
function normalizeStringArray (value: any): string[] {
|
||||
if (_.isArray(value)) return value as string[]
|
||||
if (_.isString(value)) return [value as string]
|
||||
return []
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
import Context from './context/context'
|
||||
import * as Types from './types'
|
||||
import fs from './fs/node'
|
||||
import * as _ from './util/underscore'
|
||||
import ITemplate from './template/itemplate'
|
||||
import Tokenizer from './parser/tokenizer'
|
||||
import Render from './render/render'
|
||||
import Tag from './template/tag/tag'
|
||||
import { Filter } from './template/filter/filter'
|
||||
import Parser from './parser/parser'
|
||||
import ITagImplOptions from './template/tag/itag-impl-options'
|
||||
import Value from './template/value'
|
||||
import { isTruthy, isFalsy, evalExp, evalValue } from './render/syntax'
|
||||
import builtinTags from './builtin/tags'
|
||||
import builtinFilters from './builtin/filters'
|
||||
import { LiquidOptions, NormalizedFullOptions, applyDefault, normalize } from './liquid-options'
|
||||
import { FilterImplOptions } from './template/filter/filter-impl-options'
|
||||
import IFS from './fs/ifs'
|
||||
|
||||
export default class Liquid {
|
||||
public options: NormalizedFullOptions
|
||||
public renderer: Render
|
||||
public parser: Parser
|
||||
private cache: object = {}
|
||||
private tokenizer: Tokenizer
|
||||
private fs: IFS
|
||||
|
||||
constructor (opts: LiquidOptions = {}) {
|
||||
this.options = applyDefault(normalize(opts))
|
||||
this.parser = new Parser(this)
|
||||
this.renderer = new Render()
|
||||
this.tokenizer = new Tokenizer(this.options)
|
||||
this.fs = opts.fs || fs
|
||||
|
||||
_.forOwn(builtinTags, (conf, name) => this.registerTag(name, conf))
|
||||
_.forOwn(builtinFilters, (handler, name) => this.registerFilter(name, handler))
|
||||
}
|
||||
parse (html: string, filepath?: string) {
|
||||
const tokens = this.tokenizer.tokenize(html, filepath)
|
||||
return this.parser.parse(tokens)
|
||||
}
|
||||
render (tpl: Array<ITemplate>, ctx?: object, opts?: LiquidOptions) {
|
||||
const options = { ...this.options, ...normalize(opts) }
|
||||
const scope = new Context(ctx, options)
|
||||
return this.renderer.renderTemplates(tpl, scope)
|
||||
}
|
||||
async parseAndRender (html: string, ctx?: object, opts?: LiquidOptions) {
|
||||
const tpl = await this.parse(html)
|
||||
return this.render(tpl, ctx, opts)
|
||||
}
|
||||
async getTemplate (file: string, opts?: LiquidOptions) {
|
||||
const options = normalize(opts)
|
||||
const roots = options.root ? [...options.root, ...this.options.root] : this.options.root
|
||||
const paths = roots.map(root => this.fs.resolve(root, file, this.options.extname))
|
||||
|
||||
for (const filepath of paths) {
|
||||
if (this.options.cache && this.cache[filepath]) return this.cache[filepath]
|
||||
|
||||
if (!(await this.fs.exists(filepath))) continue
|
||||
|
||||
const value = this.parse(await this.fs.readFile(filepath), filepath)
|
||||
if (this.options.cache) this.cache[filepath] = value
|
||||
return value
|
||||
}
|
||||
|
||||
const err = new Error('ENOENT') as any
|
||||
err.message = `ENOENT: Failed to lookup "${file}" in "${roots}"`
|
||||
err.code = 'ENOENT'
|
||||
throw err
|
||||
}
|
||||
async renderFile (file: string, ctx?: object, opts?: LiquidOptions) {
|
||||
const options = normalize(opts)
|
||||
const templates = await this.getTemplate(file, options)
|
||||
return this.render(templates, ctx, opts)
|
||||
}
|
||||
evalValue (str: string, ctx: Context) {
|
||||
return new Value(str, this.options.strictFilters).value(ctx)
|
||||
}
|
||||
registerFilter (name: string, filter: FilterImplOptions) {
|
||||
return Filter.register(name, filter)
|
||||
}
|
||||
registerTag (name: string, tag: ITagImplOptions) {
|
||||
return Tag.register(name, tag)
|
||||
}
|
||||
plugin (plugin: (this: Liquid, L: typeof Liquid) => void) {
|
||||
return plugin.call(this, Liquid)
|
||||
}
|
||||
express () {
|
||||
const self = this
|
||||
return function (this: any, filePath: string, ctx: object, cb: (err: Error | null, html?: string) => void) {
|
||||
const opts = { root: this.root }
|
||||
self.renderFile(filePath, ctx, opts).then(html => cb(null, html), cb)
|
||||
}
|
||||
}
|
||||
static default = Liquid
|
||||
static isTruthy = isTruthy
|
||||
static isFalsy = isFalsy
|
||||
static evalExp = evalExp
|
||||
static evalValue = evalValue
|
||||
static Types = Types
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
export default function (isTruthy) {
|
||||
return {
|
||||
'==': (l, r) => l === r,
|
||||
'!=': (l, r) => l !== r,
|
||||
'>': (l, r) => l !== null && r !== null && l > r,
|
||||
'<': (l, r) => l !== null && r !== null && l < r,
|
||||
'>=': (l, r) => l !== null && r !== null && l >= r,
|
||||
'<=': (l, r) => l !== null && r !== null && l <= r,
|
||||
'contains': (l, r) => {
|
||||
if (!l) return false
|
||||
if (typeof l.indexOf !== 'function') return false
|
||||
return l.indexOf(r) > -1
|
||||
},
|
||||
'and': (l, r) => isTruthy(l) && isTruthy(r),
|
||||
'or': (l, r) => isTruthy(l) || isTruthy(r)
|
||||
}
|
||||
}
|
||||
-106
@@ -1,106 +0,0 @@
|
||||
import * as lexical from './lexical.js'
|
||||
import {create} from './util/underscore.js'
|
||||
import {ParseError} from './util/error.js'
|
||||
import assert from './util/assert.js'
|
||||
|
||||
export default function (Tag, Filter) {
|
||||
const stream = {
|
||||
init: function (tokens) {
|
||||
this.tokens = tokens
|
||||
this.handlers = {}
|
||||
return this
|
||||
},
|
||||
on: function (name, cb) {
|
||||
this.handlers[name] = cb
|
||||
return this
|
||||
},
|
||||
trigger: function (event, arg) {
|
||||
const h = this.handlers[event]
|
||||
if (typeof h === 'function') {
|
||||
h(arg)
|
||||
return true
|
||||
}
|
||||
},
|
||||
start: function () {
|
||||
this.trigger('start')
|
||||
let token
|
||||
while (!this.stopRequested && (token = this.tokens.shift())) {
|
||||
if (this.trigger('token', token)) continue
|
||||
if (token.type === 'tag' &&
|
||||
this.trigger(`tag:${token.name}`, token)) {
|
||||
continue
|
||||
}
|
||||
const template = parseToken(token, this.tokens)
|
||||
this.trigger('template', template)
|
||||
}
|
||||
if (!this.stopRequested) this.trigger('end')
|
||||
return this
|
||||
},
|
||||
stop: function () {
|
||||
this.stopRequested = true
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
function parse (tokens) {
|
||||
let token
|
||||
const templates = []
|
||||
while ((token = tokens.shift())) {
|
||||
templates.push(parseToken(token, tokens))
|
||||
}
|
||||
return templates
|
||||
}
|
||||
|
||||
function parseToken (token, tokens) {
|
||||
try {
|
||||
let tpl = null
|
||||
if (token.type === 'tag') {
|
||||
tpl = parseTag(token, tokens)
|
||||
} else if (token.type === 'value') {
|
||||
tpl = parseValue(token.value)
|
||||
} else { // token.type === 'html'
|
||||
tpl = token
|
||||
}
|
||||
tpl.token = token
|
||||
return tpl
|
||||
} catch (e) {
|
||||
throw new ParseError(e, token)
|
||||
}
|
||||
}
|
||||
|
||||
function parseTag (token, tokens) {
|
||||
if (token.name === 'continue' || token.name === 'break') return token
|
||||
return Tag.construct(token, tokens)
|
||||
}
|
||||
|
||||
function parseValue (str) {
|
||||
let match = lexical.matchValue(str)
|
||||
assert(match, `illegal value string: ${str}`)
|
||||
|
||||
const initial = match[0]
|
||||
str = str.substr(match.index + match[0].length)
|
||||
|
||||
const filters = []
|
||||
while ((match = lexical.filter.exec(str))) {
|
||||
filters.push([match[0].trim()])
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'value',
|
||||
initial: initial,
|
||||
filters: filters.map(str => Filter.construct(str))
|
||||
}
|
||||
}
|
||||
|
||||
function parseStream (tokens) {
|
||||
const s = create(stream)
|
||||
return s.init(tokens)
|
||||
}
|
||||
|
||||
return {
|
||||
parse,
|
||||
parseTag,
|
||||
parseStream,
|
||||
parseValue
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import Token from './token'
|
||||
import { last } from '../util/underscore'
|
||||
|
||||
export default class DelimitedToken extends Token {
|
||||
constructor (
|
||||
raw: string,
|
||||
value: string,
|
||||
input: string,
|
||||
line: number,
|
||||
pos: number,
|
||||
trimLeft: boolean,
|
||||
trimRight: boolean,
|
||||
file?: string
|
||||
) {
|
||||
super(raw, input, line, pos, file)
|
||||
const tl = value[0] === '-'
|
||||
const tr = last(value) === '-'
|
||||
this.value = value
|
||||
.slice(
|
||||
tl ? 1 : 0,
|
||||
tr ? -1 : value.length
|
||||
)
|
||||
.trim()
|
||||
this.trimLeft = tl || trimLeft
|
||||
this.trimRight = tr || trimRight
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import Token from './token'
|
||||
|
||||
export default class HTMLToken extends Token {
|
||||
constructor (str: string, input: string, line: number, col: number, file?: string) {
|
||||
super(str, input, line, col, file)
|
||||
this.type = 'html'
|
||||
this.value = str
|
||||
}
|
||||
static is (token: Token): token is HTMLToken {
|
||||
return token.type === 'html'
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,7 @@ export const quoted = new RegExp(`${singleQuoted.source}|${doubleQuoted.source}`
|
||||
export const quoteBalanced = new RegExp(`(?:${quoted.source}|[^'"])*`)
|
||||
|
||||
// basic types
|
||||
export const integer = /-?\d+/
|
||||
export const number = /-?\d+\.?\d*|\.?\d+/
|
||||
export const number = /[+-]?(?:\d+\.?\d*|\.?\d+)/
|
||||
export const bool = /true|false/
|
||||
|
||||
// property access
|
||||
@@ -27,21 +26,11 @@ export const hash = new RegExp(`(?:${identifier.source})\\s*:\\s*(?:${value.sour
|
||||
export const hashCapture = new RegExp(`(${identifier.source})\\s*:\\s*(${value.source})`, 'g')
|
||||
|
||||
// full match
|
||||
export const tagLine = new RegExp(`^\\s*(${identifier.source})\\s*([\\s\\S]*)\\s*$`)
|
||||
export const literalLine = new RegExp(`^${literal.source}$`, 'i')
|
||||
export const variableLine = new RegExp(`^${variable.source}$`)
|
||||
export const tagLine = new RegExp(`^\\s*(${identifier.source})\\s*([\\s\\S]*?)\\s*$`)
|
||||
export const numberLine = new RegExp(`^${number.source}$`)
|
||||
export const boolLine = new RegExp(`^${bool.source}$`, 'i')
|
||||
export const quotedLine = new RegExp(`^${quoted.source}$`)
|
||||
export const rangeLine = new RegExp(`^${rangeCapture.source}$`)
|
||||
export const integerLine = new RegExp(`^${integer.source}$`)
|
||||
|
||||
// filter related
|
||||
export const valueDeclaration = new RegExp(`(?:${identifier.source}\\s*:\\s*)?${value.source}`)
|
||||
export const valueList = new RegExp(`${valueDeclaration.source}(\\s*,\\s*${valueDeclaration.source})*`)
|
||||
export const filter = new RegExp(`${identifier.source}(?:\\s*:\\s*${valueList.source})?`, 'g')
|
||||
export const filterCapture = new RegExp(`(${identifier.source})(?:\\s*:\\s*(${valueList.source}))?`)
|
||||
export const filterLine = new RegExp(`^${filterCapture.source}$`)
|
||||
|
||||
export const operators = [
|
||||
/\s+or\s+/,
|
||||
@@ -49,38 +38,6 @@ export const operators = [
|
||||
/==|!=|<=|>=|<|>|\s+contains\s+/
|
||||
]
|
||||
|
||||
export function isInteger (str) {
|
||||
return integerLine.test(str)
|
||||
}
|
||||
|
||||
export function isLiteral (str) {
|
||||
return literalLine.test(str)
|
||||
}
|
||||
|
||||
export function isRange (str) {
|
||||
export function isRange (str: string) {
|
||||
return rangeLine.test(str)
|
||||
}
|
||||
|
||||
export function isVariable (str) {
|
||||
return variableLine.test(str)
|
||||
}
|
||||
|
||||
export function matchValue (str) {
|
||||
return value.exec(str)
|
||||
}
|
||||
|
||||
export function parseLiteral (str) {
|
||||
let res = str.match(numberLine)
|
||||
if (res) {
|
||||
return Number(str)
|
||||
}
|
||||
res = str.match(boolLine)
|
||||
if (res) {
|
||||
return str.toLowerCase() === 'true'
|
||||
}
|
||||
res = str.match(quotedLine)
|
||||
if (res) {
|
||||
return str.slice(1, -1)
|
||||
}
|
||||
throw new TypeError(`cannot parse '${str}' as literal`)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import DelimitedToken from './delimited-token'
|
||||
import Token from './token'
|
||||
import { NormalizedFullOptions } from '../liquid-options'
|
||||
|
||||
export default class OutputToken extends DelimitedToken {
|
||||
constructor (
|
||||
raw: string,
|
||||
value: string,
|
||||
input: string,
|
||||
line: number,
|
||||
pos: number,
|
||||
options: NormalizedFullOptions,
|
||||
file?: string
|
||||
) {
|
||||
super(raw, value, input, line, pos, options.trimOutputLeft, options.trimOutputRight, file)
|
||||
this.type = 'output'
|
||||
}
|
||||
static is (token: Token): token is OutputToken {
|
||||
return token.type === 'output'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import Token from '../parser/token'
|
||||
import ITemplate from '../template/itemplate'
|
||||
import TagToken from './tag-token'
|
||||
|
||||
type ParseToken = ((token: Token, remainTokens: Array<Token>) => ITemplate)
|
||||
|
||||
export default class ParseStream {
|
||||
private tokens: Array<Token>
|
||||
private handlers: {[key: string]: (arg: any) => void} = {}
|
||||
private stopRequested: boolean = false
|
||||
private parseToken: ParseToken
|
||||
|
||||
constructor (tokens: Array<Token>, parseToken: ParseToken) {
|
||||
this.tokens = tokens
|
||||
this.parseToken = parseToken
|
||||
}
|
||||
on<T extends ITemplate | Token | undefined> (name: string, cb: (arg: T) => void): ParseStream {
|
||||
this.handlers[name] = cb
|
||||
return this
|
||||
}
|
||||
private trigger <T extends Token | ITemplate> (event: string, arg?: T) {
|
||||
const h = this.handlers[event]
|
||||
return h ? (h(arg), true) : false
|
||||
}
|
||||
start () {
|
||||
this.trigger('start')
|
||||
let token: Token | undefined
|
||||
while (!this.stopRequested && (token = this.tokens.shift())) {
|
||||
if (this.trigger('token', token)) continue
|
||||
if (TagToken.is(token) && this.trigger(`tag:${token.name}`, token)) {
|
||||
continue
|
||||
}
|
||||
const template = this.parseToken(token, this.tokens)
|
||||
this.trigger('template', template)
|
||||
}
|
||||
if (!this.stopRequested) this.trigger('end')
|
||||
return this
|
||||
}
|
||||
stop () {
|
||||
this.stopRequested = true
|
||||
return this
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { ParseError } from '../util/error'
|
||||
import Liquid from '../liquid'
|
||||
import ParseStream from './parse-stream'
|
||||
import Token from './token'
|
||||
import TagToken from './tag-token'
|
||||
import OutputToken from './output-token'
|
||||
import Tag from '../template/tag/tag'
|
||||
import Output from '../template/output'
|
||||
import HTML from '../template/html'
|
||||
import ITemplate from '../template/itemplate'
|
||||
|
||||
export default class Parser {
|
||||
liquid: Liquid
|
||||
|
||||
constructor (liquid: Liquid) {
|
||||
this.liquid = liquid
|
||||
}
|
||||
parse (tokens: Array<Token>) {
|
||||
let token
|
||||
const templates: ITemplate[] = []
|
||||
while ((token = tokens.shift())) {
|
||||
templates.push(this.parseToken(token, tokens))
|
||||
}
|
||||
return templates
|
||||
}
|
||||
parseToken (token: Token, remainTokens: Array<Token>) {
|
||||
try {
|
||||
if (TagToken.is(token)) {
|
||||
return new Tag(token, remainTokens, this.liquid)
|
||||
}
|
||||
if (OutputToken.is(token)) {
|
||||
return new Output(token as OutputToken, this.liquid.options.strictFilters)
|
||||
}
|
||||
return new HTML(token)
|
||||
} catch (e) {
|
||||
throw new ParseError(e, token)
|
||||
}
|
||||
}
|
||||
parseStream (tokens: Array<Token>) {
|
||||
return new ParseStream(tokens, (token, tokens) => this.parseToken(token, tokens))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import DelimitedToken from './delimited-token'
|
||||
import Token from './token'
|
||||
import { TokenizationError } from '../util/error'
|
||||
import * as lexical from './lexical'
|
||||
import { NormalizedFullOptions } from '../liquid-options'
|
||||
|
||||
export default class TagToken extends DelimitedToken {
|
||||
name: string
|
||||
args: string
|
||||
constructor (
|
||||
raw: string,
|
||||
value: string,
|
||||
input: string,
|
||||
line: number,
|
||||
pos: number,
|
||||
options: NormalizedFullOptions,
|
||||
file?: string
|
||||
) {
|
||||
super(raw, value, input, line, pos, options.trimTagLeft, options.trimTagRight, file)
|
||||
this.type = 'tag'
|
||||
const match = this.value.match(lexical.tagLine)
|
||||
if (!match) {
|
||||
throw new TokenizationError(`illegal tag syntax`, this)
|
||||
}
|
||||
this.name = match[1]
|
||||
this.args = match[2]
|
||||
}
|
||||
static is (token: Token): token is TagToken {
|
||||
return token.type === 'tag'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
export default class Token {
|
||||
trimLeft: boolean = false
|
||||
trimRight: boolean = false
|
||||
type: string = 'notset'
|
||||
line: number
|
||||
col: number
|
||||
raw: string
|
||||
input: string
|
||||
file?: string
|
||||
value: string
|
||||
constructor (raw: string, input: string, line: number, col: number, file?: string) {
|
||||
this.col = col
|
||||
this.line = line
|
||||
this.raw = raw
|
||||
this.value = raw
|
||||
this.input = input
|
||||
this.file = file
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import whiteSpaceCtrl from './whitespace-ctrl'
|
||||
import HTMLToken from './html-token'
|
||||
import TagToken from './tag-token'
|
||||
import Token from './token'
|
||||
import OutputToken from './output-token'
|
||||
import { TokenizationError } from '../util/error'
|
||||
import { NormalizedFullOptions, applyDefault } from '../liquid-options'
|
||||
|
||||
enum ParseState { HTML, OUTPUT, TAG }
|
||||
|
||||
export default class Tokenizer {
|
||||
private options: NormalizedFullOptions
|
||||
constructor (options?: NormalizedFullOptions) {
|
||||
this.options = applyDefault(options)
|
||||
}
|
||||
tokenize (input: string, file?: string) {
|
||||
const tokens: Token[] = []
|
||||
const {
|
||||
tagDelimiterLeft,
|
||||
tagDelimiterRight,
|
||||
outputDelimiterLeft,
|
||||
outputDelimiterRight
|
||||
} = this.options
|
||||
let p = 0
|
||||
let curLine = 1
|
||||
let state = ParseState.HTML
|
||||
let buffer = ''
|
||||
let lineBegin = 0
|
||||
let line = 1
|
||||
let col = 1
|
||||
|
||||
while (p < input.length) {
|
||||
if (input[p] === '\n') {
|
||||
curLine++
|
||||
lineBegin = p + 1
|
||||
}
|
||||
if (state === ParseState.HTML) {
|
||||
if (input.substr(p, outputDelimiterLeft.length) === outputDelimiterLeft) {
|
||||
if (buffer) tokens.push(new HTMLToken(buffer, input, line, col, file))
|
||||
buffer = outputDelimiterLeft
|
||||
line = curLine
|
||||
col = p - lineBegin + 1
|
||||
p += outputDelimiterLeft.length
|
||||
state = ParseState.OUTPUT
|
||||
continue
|
||||
} else if (input.substr(p, tagDelimiterLeft.length) === tagDelimiterLeft) {
|
||||
if (buffer) tokens.push(new HTMLToken(buffer, input, line, col, file))
|
||||
buffer = tagDelimiterLeft
|
||||
line = curLine
|
||||
col = p - lineBegin + 1
|
||||
p += tagDelimiterLeft.length
|
||||
state = ParseState.TAG
|
||||
continue
|
||||
}
|
||||
} else if (
|
||||
state === ParseState.OUTPUT &&
|
||||
input.substr(p, outputDelimiterRight.length) === outputDelimiterRight
|
||||
) {
|
||||
buffer += outputDelimiterRight
|
||||
tokens.push(new OutputToken(buffer, buffer.slice(outputDelimiterLeft.length, -outputDelimiterRight.length), input, line, col, this.options, file))
|
||||
p += outputDelimiterRight.length
|
||||
buffer = ''
|
||||
line = curLine
|
||||
col = p - lineBegin + 1
|
||||
state = ParseState.HTML
|
||||
continue
|
||||
} else if (input.substr(p, tagDelimiterRight.length) === tagDelimiterRight) {
|
||||
buffer += tagDelimiterRight
|
||||
tokens.push(new TagToken(buffer, buffer.slice(tagDelimiterLeft.length, -tagDelimiterRight.length), input, line, col, this.options, file))
|
||||
p += tagDelimiterRight.length
|
||||
buffer = ''
|
||||
line = curLine
|
||||
col = p - lineBegin + 1
|
||||
state = ParseState.HTML
|
||||
continue
|
||||
}
|
||||
buffer += input[p++]
|
||||
}
|
||||
if (state !== ParseState.HTML) {
|
||||
const t = state === ParseState.OUTPUT ? 'output' : 'tag'
|
||||
const str = buffer.length > 16 ? buffer.slice(0, 13) + '...' : buffer
|
||||
throw new TokenizationError(
|
||||
`${t} "${str}" not closed`,
|
||||
new Token(buffer, input, line, col, file)
|
||||
)
|
||||
}
|
||||
if (buffer) tokens.push(new HTMLToken(buffer, input, line, col, file))
|
||||
|
||||
whiteSpaceCtrl(tokens, this.options)
|
||||
return tokens
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import Token from '../parser/token'
|
||||
import TagToken from '../parser/tag-token'
|
||||
import HTMLToken from '../parser/html-token'
|
||||
import { NormalizedFullOptions } from '../liquid-options'
|
||||
|
||||
export default function whiteSpaceCtrl (tokens: Token[], options: NormalizedFullOptions) {
|
||||
options = { greedy: true, ...options }
|
||||
let inRaw = false
|
||||
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
const token = tokens[i]
|
||||
if (!inRaw && token.trimLeft) {
|
||||
trimLeft(tokens[i - 1], options.greedy)
|
||||
}
|
||||
|
||||
if (TagToken.is(token)) {
|
||||
if (token.name === 'raw') inRaw = true
|
||||
else if (token.name === 'endraw') inRaw = false
|
||||
}
|
||||
|
||||
if (!inRaw && token.trimRight) {
|
||||
trimRight(tokens[i + 1], options.greedy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function trimLeft (token: Token, greedy: boolean) {
|
||||
if (!token || !HTMLToken.is(token)) return
|
||||
|
||||
const rLeft = greedy ? /\s+$/g : /[\t\r ]*$/g
|
||||
token.value = token.value.replace(rLeft, '')
|
||||
}
|
||||
|
||||
function trimRight (token: Token, greedy: boolean) {
|
||||
if (!token || !HTMLToken.is(token)) return
|
||||
|
||||
const rRight = greedy ? /^\s+/g : /^[\t\r ]*\n?/g
|
||||
token.value = token.value.replace(rRight, '')
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import {evalExp} from './syntax.js'
|
||||
import {RenderBreakError, RenderError} from './util/error.js'
|
||||
import {stringify, create} from './util/underscore.js'
|
||||
import assert from './util/assert.js'
|
||||
|
||||
const render = {
|
||||
renderTemplates: async function (templates, scope) {
|
||||
assert(scope, 'unable to evalTemplates: scope undefined')
|
||||
|
||||
let html = ''
|
||||
for (const tpl of templates) {
|
||||
try {
|
||||
html += await renderTemplate.call(this, tpl)
|
||||
} catch (e) {
|
||||
if (e instanceof RenderBreakError) {
|
||||
e.resolvedHTML = html
|
||||
throw e
|
||||
}
|
||||
throw new RenderError(e, tpl)
|
||||
}
|
||||
}
|
||||
return html
|
||||
|
||||
async function renderTemplate (template) {
|
||||
if (template.type === 'tag') {
|
||||
const partial = await this.renderTag(template, scope)
|
||||
return partial === undefined ? '' : partial
|
||||
}
|
||||
if (template.type === 'value') {
|
||||
return this.renderValue(template, scope)
|
||||
}
|
||||
return template.value
|
||||
}
|
||||
},
|
||||
|
||||
renderTag: async function (template, scope) {
|
||||
if (template.name === 'continue') {
|
||||
throw new RenderBreakError('continue')
|
||||
}
|
||||
if (template.name === 'break') {
|
||||
throw new RenderBreakError('break')
|
||||
}
|
||||
return template.render(scope)
|
||||
},
|
||||
|
||||
renderValue: async function (template, scope) {
|
||||
const partial = this.evalValue(template, scope)
|
||||
return partial === undefined ? '' : stringify(partial)
|
||||
},
|
||||
|
||||
evalValue: function (template, scope) {
|
||||
assert(scope, 'unable to evalValue: scope undefined')
|
||||
return template.filters.reduce(
|
||||
(prev, filter) => filter.render(prev, scope),
|
||||
evalExp(template.initial, scope))
|
||||
}
|
||||
}
|
||||
|
||||
export default function () {
|
||||
const instance = create(render)
|
||||
return instance
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { RenderError } from '../util/error'
|
||||
import assert from '../util/assert'
|
||||
import Context from '../context/context'
|
||||
import ITemplate from '../template/itemplate'
|
||||
|
||||
export default class Render {
|
||||
async renderTemplates (templates: ITemplate[], ctx: Context) {
|
||||
assert(ctx, 'unable to evalTemplates: context undefined')
|
||||
|
||||
let html = ''
|
||||
for (const tpl of templates) {
|
||||
try {
|
||||
html += await tpl.render(ctx)
|
||||
} catch (e) {
|
||||
if (e.name === 'RenderBreakError') {
|
||||
e.resolvedHTML = html
|
||||
throw e
|
||||
}
|
||||
throw e.name === 'RenderError' ? e : new RenderError(e, tpl)
|
||||
}
|
||||
}
|
||||
return html
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import * as lexical from '../parser/lexical'
|
||||
import assert from '../util/assert'
|
||||
import Context from '../context/context'
|
||||
import { range, last, isFunction, toValue } from '../util/underscore'
|
||||
import { isComparable } from '../drop/icomparable'
|
||||
import { NullDrop } from '../drop/null-drop'
|
||||
import { EmptyDrop } from '../drop/empty-drop'
|
||||
import { BlankDrop } from '../drop/blank-drop'
|
||||
|
||||
const binaryOperators: {[key: string]: (lhs: any, rhs: any) => boolean} = {
|
||||
'==': (l: any, r: any) => {
|
||||
if (isComparable(l)) return l.equals(r)
|
||||
if (isComparable(r)) return r.equals(l)
|
||||
return l === r
|
||||
},
|
||||
'!=': (l: any, r: any) => {
|
||||
if (isComparable(l)) return !l.equals(r)
|
||||
if (isComparable(r)) return !r.equals(l)
|
||||
return l !== r
|
||||
},
|
||||
'>': (l: any, r: any) => {
|
||||
if (isComparable(l)) return l.gt(r)
|
||||
if (isComparable(r)) return r.lt(l)
|
||||
return l > r
|
||||
},
|
||||
'<': (l: any, r: any) => {
|
||||
if (isComparable(l)) return l.lt(r)
|
||||
if (isComparable(r)) return r.gt(l)
|
||||
return l < r
|
||||
},
|
||||
'>=': (l: any, r: any) => {
|
||||
if (isComparable(l)) return l.geq(r)
|
||||
if (isComparable(r)) return r.leq(l)
|
||||
return l >= r
|
||||
},
|
||||
'<=': (l: any, r: any) => {
|
||||
if (isComparable(l)) return l.leq(r)
|
||||
if (isComparable(r)) return r.geq(l)
|
||||
return l <= r
|
||||
},
|
||||
'contains': (l: any, r: any) => {
|
||||
return l && isFunction(l.indexOf) ? l.indexOf(r) > -1 : false
|
||||
},
|
||||
'and': (l: any, r: any) => isTruthy(l) && isTruthy(r),
|
||||
'or': (l: any, r: any) => isTruthy(l) || isTruthy(r)
|
||||
}
|
||||
|
||||
export async function parseExp (exp: string, ctx: Context): Promise<any> {
|
||||
assert(ctx, 'unable to parseExp: scope undefined')
|
||||
const operatorREs = lexical.operators
|
||||
let match
|
||||
for (let i = 0; i < operatorREs.length; i++) {
|
||||
const operatorRE = operatorREs[i]
|
||||
const expRE = new RegExp(`^(${lexical.quoteBalanced.source})(${operatorRE.source})(${lexical.quoteBalanced.source})$`)
|
||||
if ((match = exp.match(expRE))) {
|
||||
const l = await parseExp(match[1], ctx)
|
||||
const op = binaryOperators[match[2].trim()]
|
||||
const r = await parseExp(match[3], ctx)
|
||||
return op(l, r)
|
||||
}
|
||||
}
|
||||
|
||||
if ((match = exp.match(lexical.rangeLine))) {
|
||||
const low = await evalValue(match[1], ctx)
|
||||
const high = await evalValue(match[2], ctx)
|
||||
return range(+low, +high + 1)
|
||||
}
|
||||
|
||||
return parseValue(exp, ctx)
|
||||
}
|
||||
|
||||
export async function evalExp (str: string, ctx: Context): Promise<any> {
|
||||
return toValue(await parseExp(str, ctx))
|
||||
}
|
||||
|
||||
export async function parseValue (str: string | undefined, ctx: Context): Promise<any> {
|
||||
if (!str) return null
|
||||
str = str.trim()
|
||||
|
||||
if (str === 'true') return true
|
||||
if (str === 'false') return false
|
||||
if (str === 'nil' || str === 'null') return new NullDrop()
|
||||
if (str === 'empty') return new EmptyDrop()
|
||||
if (str === 'blank') return new BlankDrop()
|
||||
if (!isNaN(Number(str))) return Number(str)
|
||||
if ((str[0] === '"' || str[0] === "'") && str[0] === last(str)) return str.slice(1, -1)
|
||||
return ctx.get(str)
|
||||
}
|
||||
|
||||
export async function evalValue (str: string | undefined, ctx: Context) {
|
||||
return toValue(await parseValue(str, ctx))
|
||||
}
|
||||
|
||||
export function isTruthy (val: any): boolean {
|
||||
return !isFalsy(val)
|
||||
}
|
||||
|
||||
export function isFalsy (val: any): boolean {
|
||||
return val === false || undefined === val || val === null
|
||||
}
|
||||
-169
@@ -1,169 +0,0 @@
|
||||
import * as _ from './util/underscore.js'
|
||||
import * as lexical from './lexical.js'
|
||||
import assert from './util/assert.js'
|
||||
|
||||
const Scope = {
|
||||
getAll: function () {
|
||||
return this.contexts.reduce((ctx, val) => _.assign(ctx, val), _.create(null))
|
||||
},
|
||||
get: function (path) {
|
||||
const paths = this.propertyAccessSeq(path)
|
||||
const scope = this.findContextFor(paths[0]) || _.last(this.contexts)
|
||||
return paths.reduce((value, key) => this.readProperty(value, key), scope)
|
||||
},
|
||||
set: function (path, v) {
|
||||
const paths = this.propertyAccessSeq(path)
|
||||
let scope = this.findContextFor(paths[0]) || _.last(this.contexts)
|
||||
paths.some((key, i) => {
|
||||
if (!_.isObject(scope)) {
|
||||
return true
|
||||
}
|
||||
if (i === paths.length - 1) {
|
||||
scope[key] = v
|
||||
return true
|
||||
}
|
||||
if (undefined === scope[key]) {
|
||||
scope[key] = {}
|
||||
}
|
||||
scope = scope[key]
|
||||
})
|
||||
},
|
||||
unshift: function (ctx) {
|
||||
return this.contexts.unshift(ctx)
|
||||
},
|
||||
push: function (ctx) {
|
||||
return this.contexts.push(ctx)
|
||||
},
|
||||
pop: function (ctx) {
|
||||
if (!arguments.length) {
|
||||
return this.contexts.pop()
|
||||
}
|
||||
const i = this.contexts.findIndex(scope => scope === ctx)
|
||||
if (i === -1) {
|
||||
throw new TypeError('scope not found, cannot pop')
|
||||
}
|
||||
return this.contexts.splice(i, 1)[0]
|
||||
},
|
||||
findContextFor: function (key, filter) {
|
||||
filter = filter || (() => true)
|
||||
for (let i = this.contexts.length - 1; i >= 0; i--) {
|
||||
const candidate = this.contexts[i]
|
||||
if (!filter(candidate)) continue
|
||||
if (key in candidate) {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
return null
|
||||
},
|
||||
readProperty: function (obj, key) {
|
||||
let val
|
||||
if (_.isNil(obj)) {
|
||||
val = undefined
|
||||
} else {
|
||||
if (typeof obj.to_liquid === 'function') {
|
||||
obj = obj.to_liquid()
|
||||
} else if (typeof obj.toLiquid === 'function') {
|
||||
obj = obj.toLiquid()
|
||||
}
|
||||
|
||||
if (key === 'size' && (_.isArray(obj) || _.isString(obj))) {
|
||||
val = obj.length
|
||||
} else {
|
||||
val = obj[key]
|
||||
}
|
||||
}
|
||||
if (_.isNil(val) && this.opts.strict_variables) {
|
||||
throw new TypeError(`undefined variable: ${key}`)
|
||||
}
|
||||
return val
|
||||
},
|
||||
|
||||
/*
|
||||
* Parse property access sequence from access string
|
||||
* @example
|
||||
* accessSeq("foo.bar") // ['foo', 'bar']
|
||||
* accessSeq("foo['bar']") // ['foo', 'bar']
|
||||
* accessSeq("foo['b]r']") // ['foo', 'b]r']
|
||||
* accessSeq("foo[bar.coo]") // ['foo', 'bar'], for bar.coo == 'bar'
|
||||
*/
|
||||
propertyAccessSeq: function (str) {
|
||||
str = String(str)
|
||||
const seq = []
|
||||
let name = ''
|
||||
let j
|
||||
let i = 0
|
||||
while (i < str.length) {
|
||||
switch (str[i]) {
|
||||
case '[':
|
||||
push()
|
||||
|
||||
const delemiter = str[i + 1]
|
||||
if (/['"]/.test(delemiter)) { // foo["bar"]
|
||||
j = str.indexOf(delemiter, i + 2)
|
||||
assert(j !== -1, `unbalanced ${delemiter}: ${str}`)
|
||||
name = str.slice(i + 2, j)
|
||||
push()
|
||||
i = j + 2
|
||||
} else { // foo[bar.coo]
|
||||
j = matchRightBracket(str, i + 1)
|
||||
assert(j !== -1, `unbalanced []: ${str}`)
|
||||
name = str.slice(i + 1, j)
|
||||
if (!lexical.isInteger(name)) { // foo[bar] vs. foo[1]
|
||||
name = String(this.get(name))
|
||||
}
|
||||
push()
|
||||
i = j + 1
|
||||
}
|
||||
break
|
||||
case '.':// foo.bar, foo[0].bar
|
||||
push()
|
||||
i++
|
||||
break
|
||||
default:// foo.bar
|
||||
name += str[i]
|
||||
i++
|
||||
}
|
||||
}
|
||||
push()
|
||||
|
||||
if (!seq.length) {
|
||||
throw new TypeError(`invalid path:"${str}"`)
|
||||
}
|
||||
return seq
|
||||
|
||||
function push () {
|
||||
if (name.length) seq.push(name)
|
||||
name = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function matchRightBracket (str, begin) {
|
||||
let stack = 1 // count of '[' - count of ']'
|
||||
for (let i = begin; i < str.length; i++) {
|
||||
if (str[i] === '[') {
|
||||
stack++
|
||||
}
|
||||
if (str[i] === ']') {
|
||||
stack--
|
||||
if (stack === 0) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
export function factory (ctx, opts) {
|
||||
const defaultOptions = {
|
||||
dynamicPartials: true,
|
||||
strict_variables: false,
|
||||
strict_filters: false,
|
||||
blocks: {},
|
||||
root: []
|
||||
}
|
||||
const scope = _.create(Scope)
|
||||
scope.opts = _.assign(defaultOptions, opts)
|
||||
scope.contexts = [ctx || {}]
|
||||
return scope
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user