mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-12 19:00:39 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -1,3 +1,76 @@
|
||||
## [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)
|
||||
|
||||
|
||||
|
||||
@@ -12,51 +12,50 @@
|
||||
[](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), 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()`
|
||||
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');
|
||||
@@ -64,35 +63,27 @@ 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 = new Liquid({
|
||||
root: path.resolve(__dirname, 'views/'), // dirs to lookup layouts/includes
|
||||
extname: '.liquid' // the extname used for layouts/includes, defaults ""
|
||||
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"
|
||||
```
|
||||
|
||||
@@ -105,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
|
||||
|
||||
```
|
||||
@@ -226,9 +202,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'
|
||||
}
|
||||
});
|
||||
```
|
||||
@@ -257,6 +233,16 @@ 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.
|
||||
@@ -269,7 +255,6 @@ This repo uses [eslint](https://eslint.org/) to check code style, [semantic-rele
|
||||
[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
|
||||
@@ -278,3 +263,4 @@ This repo uses [eslint](https://eslint.org/) to check code style, [semantic-rele
|
||||
[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/
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
import * as Benchmark from 'benchmark'
|
||||
import Liquid from '../src/liquid'
|
||||
import TagToken from '../src/parser/tag-token'
|
||||
import Scope from '../src/scope/scope'
|
||||
import Context from '../src/context/context'
|
||||
|
||||
const engine = new Liquid({
|
||||
root: __dirname,
|
||||
@@ -13,8 +13,8 @@ engine.registerTag('header', {
|
||||
const [key, val] = token.args.split(':')
|
||||
this[key] = val
|
||||
},
|
||||
render: function (scope: Scope) {
|
||||
const title = this.liquid.evalValue(this.content, scope)
|
||||
render: function (ctx: Context) {
|
||||
const title = this.liquid.evalValue(this.content, ctx)
|
||||
return `<h1>${title}</h1>`
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
declare module 'rollup-plugin-uglify' {
|
||||
export function uglify (): any
|
||||
}
|
||||
declare module 'rollup-plugin-alias' {
|
||||
export default function alias(options: any): any
|
||||
declare module 'rollup-plugin-replace' {
|
||||
export default function replace(options: any): any
|
||||
}
|
||||
|
||||
Generated
+23
-6
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "liquidjs",
|
||||
"version": "7.4.0",
|
||||
"version": "8.2.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -3745,6 +3745,15 @@
|
||||
"integrity": "sha512-iCM3ZGeqIzlrH7KxYK+fphlJpCCczyHXc+HhRVbEu9uNTCrzYJjvvtefzeKTCVHd5AP/aD/fzC80JZ4ZP+dQ/A==",
|
||||
"dev": true
|
||||
},
|
||||
"magic-string": {
|
||||
"version": "0.25.2",
|
||||
"resolved": "http://registry.npm.taobao.org/magic-string/download/magic-string-0.25.2.tgz",
|
||||
"integrity": "sha1-E5w6cpUV7FXpbmnoKhH+iQopOtk=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"sourcemap-codec": "^1.4.4"
|
||||
}
|
||||
},
|
||||
"make-error": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz",
|
||||
@@ -9136,13 +9145,15 @@
|
||||
"acorn": "^6.0.5"
|
||||
}
|
||||
},
|
||||
"rollup-plugin-alias": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/rollup-plugin-alias/-/rollup-plugin-alias-1.5.1.tgz",
|
||||
"integrity": "sha512-pQTYBRNfLedoVOO7AYHNegIavEIp4jKTga5jUi1r//KYgHKGWgG4qJXYhbcWKt2k1FwGlR5wCYoY+IFkme0t4A==",
|
||||
"rollup-plugin-replace": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "http://registry.npm.taobao.org/rollup-plugin-replace/download/rollup-plugin-replace-2.1.0.tgz",
|
||||
"integrity": "sha1-+cB6Somi+L6RLuVLPw9o2R6e0K4=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"slash": "^2.0.0"
|
||||
"magic-string": "^0.25.1",
|
||||
"minimatch": "^3.0.2",
|
||||
"rollup-pluginutils": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"rollup-plugin-typescript2": {
|
||||
@@ -9762,6 +9773,12 @@
|
||||
"integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=",
|
||||
"dev": true
|
||||
},
|
||||
"sourcemap-codec": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "http://registry.npm.taobao.org/sourcemap-codec/download/sourcemap-codec-1.4.4.tgz",
|
||||
"integrity": "sha1-xj6pJ8Ap3WvZorf6A7P+wCrVbp8=",
|
||||
"dev": true
|
||||
},
|
||||
"spawn-error-forwarder": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/spawn-error-forwarder/-/spawn-error-forwarder-1.0.0.tgz",
|
||||
|
||||
+8
-8
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"name": "liquidjs",
|
||||
"version": "7.4.0",
|
||||
"version": "8.2.1",
|
||||
"description": "Liquid template engine by pure JavaScript: compatible to shopify, easy to extend.",
|
||||
"main": "dist/liquid.common.js",
|
||||
"types": "dist/liquid.d.ts",
|
||||
"browser": "dist/liquid.js",
|
||||
"scripts": {
|
||||
"lint": "eslint . --ext .ts",
|
||||
"unit": "mocha 'test/unit/**/*.ts'",
|
||||
"integration": "mocha 'test/integration/**/*.ts'",
|
||||
"e2e": "npm run build && mocha 'test/e2e/**/*.ts'",
|
||||
"test": "npm run build && mocha 'test/**/*.ts'",
|
||||
"unit": "mocha \"test/unit/**/*.ts\"",
|
||||
"integration": "mocha \"test/integration/**/*.ts\"",
|
||||
"e2e": "npm run build && mocha \"test/e2e/**/*.ts\"",
|
||||
"test": "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",
|
||||
"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"
|
||||
},
|
||||
@@ -76,7 +76,7 @@
|
||||
"nyc": "^13.1.0",
|
||||
"regenerator-runtime": "^0.12.1",
|
||||
"rollup": "^1.1.2",
|
||||
"rollup-plugin-alias": "^1.5.1",
|
||||
"rollup-plugin-replace": "^2.1.0",
|
||||
"rollup-plugin-typescript2": "^0.19.2",
|
||||
"rollup-plugin-uglify": "^6.0.2",
|
||||
"semantic-release": "^15.13.3",
|
||||
|
||||
+11
-3
@@ -1,7 +1,7 @@
|
||||
import alias from 'rollup-plugin-alias'
|
||||
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
|
||||
@@ -45,7 +45,11 @@ export default [{
|
||||
banner
|
||||
}],
|
||||
plugins: [
|
||||
alias({ resolve: ['.ts'], './fs/node': './fs/browser' }),
|
||||
replace({
|
||||
include: './src/liquid.ts',
|
||||
delimiters: ['', ''],
|
||||
'./fs/node': './fs/browser'
|
||||
}),
|
||||
typescript({
|
||||
tsconfigOverride: {
|
||||
include: [ 'src' ],
|
||||
@@ -67,7 +71,11 @@ export default [{
|
||||
sourcemap
|
||||
}],
|
||||
plugins: [
|
||||
alias({ resolve: ['.ts'], './fs/node': './fs/browser' }),
|
||||
replace({
|
||||
include: './src/liquid.ts',
|
||||
delimiters: ['', ''],
|
||||
'./fs/node': './fs/browser'
|
||||
}),
|
||||
typescript({
|
||||
tsconfigOverride: {
|
||||
include: [ 'src' ],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { last } from '../../util/underscore'
|
||||
import { isTruthy } from '../../render/syntax'
|
||||
|
||||
export default {
|
||||
'join': (v: any[], arg: string) => v.join(arg === undefined ? ' ' : arg),
|
||||
@@ -9,8 +10,8 @@ export default {
|
||||
'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): T[] => {
|
||||
if (length === undefined) length = 1
|
||||
'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[] {
|
||||
@@ -20,5 +21,8 @@ export default {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { isTruthy } from '../../render/syntax'
|
||||
import { isFalsy } from '../../render/syntax'
|
||||
|
||||
export default {
|
||||
'default': <T1, T2>(v: T1, arg: T2): T1 | T2 => isTruthy(v) ? v : arg
|
||||
'default': <T1, T2>(v: string | T1, arg: T2): string | T1 | T2 => isFalsy(v) || v === '' ? arg : v
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { FilterImpl } from '../../template/filter/filter-impl'
|
||||
|
||||
export default {
|
||||
'append': (v: string, arg: string) => v + arg,
|
||||
'prepend': (v: string, arg: string) => arg + v,
|
||||
@@ -27,4 +25,4 @@ export default {
|
||||
if (arr.length >= l) ret += o
|
||||
return ret
|
||||
}
|
||||
} as {[key: string]: FilterImpl}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import assert from '../../util/assert'
|
||||
import { identifier } from '../../parser/lexical'
|
||||
import { AssignScope } from '../../scope/scopes'
|
||||
import TagToken from '../../parser/tag-token'
|
||||
import Scope from '../../scope/scope'
|
||||
import Context from '../../context/context'
|
||||
import ITagImplOptions from '../../template/tag/itag-impl-options'
|
||||
|
||||
const re = new RegExp(`(${identifier.source})\\s*=([^]*)`)
|
||||
@@ -14,10 +13,7 @@ export default {
|
||||
this.key = match[1]
|
||||
this.value = match[2]
|
||||
},
|
||||
render: function (scope: Scope) {
|
||||
const ctx = new AssignScope()
|
||||
ctx[this.key] = this.liquid.evalValue(this.value, scope)
|
||||
scope.push(ctx)
|
||||
return Promise.resolve('')
|
||||
render: async function (ctx: Context) {
|
||||
ctx.front()[this.key] = await this.liquid.evalValue(this.value, ctx)
|
||||
}
|
||||
} as ITagImplOptions
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import BlockMode from '../../scope/block-mode'
|
||||
import BlockMode from '../../context/block-mode'
|
||||
import TagToken from '../../parser/tag-token'
|
||||
import Token from '../../parser/token'
|
||||
import ITemplate from '../../template/itemplate'
|
||||
import Scope from '../../scope/scope'
|
||||
import Context from '../../context/context'
|
||||
import ITagImplOptions from '../../template/tag/itag-impl-options'
|
||||
import ParseStream from '../../parser/parse-stream'
|
||||
|
||||
@@ -19,14 +19,15 @@ export default {
|
||||
})
|
||||
stream.start()
|
||||
},
|
||||
render: async function (scope: Scope) {
|
||||
const childDefined = scope.blocks[this.block]
|
||||
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, scope)
|
||||
: await this.liquid.renderer.renderTemplates(this.tpls, ctx)
|
||||
|
||||
if (scope.blockMode === BlockMode.STORE) {
|
||||
scope.blocks[this.block] = html
|
||||
if (ctx.getRegister('blockMode', BlockMode.OUTPUT) === BlockMode.STORE) {
|
||||
blocks[this.block] = html
|
||||
return ''
|
||||
}
|
||||
return html
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import assert from '../../util/assert'
|
||||
import { identifier } from '../../parser/lexical'
|
||||
import { CaptureScope } from '../../scope/scopes'
|
||||
import TagToken from '../../parser/tag-token'
|
||||
import Token from '../../parser/token'
|
||||
import Scope from '../../scope/scope'
|
||||
import Context from '../../context/context'
|
||||
import ITagImplOptions from '../../template/tag/itag-impl-options'
|
||||
|
||||
const re = new RegExp(`(${identifier.source})`)
|
||||
@@ -24,10 +23,8 @@ export default {
|
||||
})
|
||||
stream.start()
|
||||
},
|
||||
render: async function (scope: Scope) {
|
||||
const html = await this.liquid.renderer.renderTemplates(this.templates, scope)
|
||||
const ctx = new CaptureScope()
|
||||
ctx[this.variable] = html
|
||||
scope.push(ctx)
|
||||
render: async function (ctx: Context) {
|
||||
const html = await this.liquid.renderer.renderTemplates(this.templates, ctx)
|
||||
ctx.front()[this.variable] = html
|
||||
}
|
||||
} as ITagImplOptions
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { evalExp } from '../../render/syntax'
|
||||
import TagToken from '../../parser/tag-token'
|
||||
import Token from '../../parser/token'
|
||||
import Scope from '../../scope/scope'
|
||||
import Context from '../../context/context'
|
||||
import ITemplate from '../../template/itemplate'
|
||||
import ITagImplOptions from '../../template/tag/itag-impl-options'
|
||||
import ParseStream from '../../parser/parse-stream'
|
||||
@@ -30,15 +30,15 @@ export default {
|
||||
stream.start()
|
||||
},
|
||||
|
||||
render: function (scope: Scope) {
|
||||
render: async function (ctx: Context) {
|
||||
for (let i = 0; i < this.cases.length; i++) {
|
||||
const branch = this.cases[i]
|
||||
const val = evalExp(branch.val, scope)
|
||||
const cond = evalExp(this.cond, scope)
|
||||
const val = await evalExp(branch.val, ctx)
|
||||
const cond = await evalExp(this.cond, ctx)
|
||||
if (val === cond) {
|
||||
return this.liquid.renderer.renderTemplates(branch.templates, scope)
|
||||
return this.liquid.renderer.renderTemplates(branch.templates, ctx)
|
||||
}
|
||||
}
|
||||
return this.liquid.renderer.renderTemplates(this.elseTemplates, scope)
|
||||
return this.liquid.renderer.renderTemplates(this.elseTemplates, ctx)
|
||||
}
|
||||
} as ITagImplOptions
|
||||
|
||||
@@ -2,7 +2,7 @@ import assert from '../../util/assert'
|
||||
import { value as rValue } from '../../parser/lexical'
|
||||
import { evalValue } from '../../render/syntax'
|
||||
import TagToken from '../../parser/tag-token'
|
||||
import Scope from '../../scope/scope'
|
||||
import Context from '../../context/context'
|
||||
import ITagImplOptions from '../../template/tag/itag-impl-options'
|
||||
|
||||
const groupRE = new RegExp(`^(?:(${rValue.source})\\s*:\\s*)?(.*)$`)
|
||||
@@ -24,10 +24,10 @@ export default <ITagImplOptions>{
|
||||
assert(this.candidates.length, `empty candidates: ${tagToken.raw}`)
|
||||
},
|
||||
|
||||
render: function (scope: Scope) {
|
||||
const group = evalValue(this.group, scope)
|
||||
render: async function (ctx: Context) {
|
||||
const group = await evalValue(this.group, ctx)
|
||||
const fingerprint = `cycle:${group}:` + this.candidates.join(',')
|
||||
const groups = scope.groups
|
||||
const groups = ctx.getRegister('cycle')
|
||||
let idx = groups[fingerprint]
|
||||
|
||||
if (idx === undefined) {
|
||||
@@ -38,6 +38,6 @@ export default <ITagImplOptions>{
|
||||
idx = (idx + 1) % this.candidates.length
|
||||
groups[fingerprint] = idx
|
||||
|
||||
return evalValue(candidate, scope)
|
||||
return evalValue(candidate, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import assert from '../../util/assert'
|
||||
import { identifier } from '../../parser/lexical'
|
||||
import { CaptureScope, AssignScope, DecrementScope } from '../../scope/scopes'
|
||||
import TagToken from '../../parser/tag-token'
|
||||
import Scope from '../../scope/scope'
|
||||
import Context from '../../context/context'
|
||||
import ITagImplOptions from '../../template/tag/itag-impl-options'
|
||||
|
||||
export default {
|
||||
@@ -11,20 +10,11 @@ export default {
|
||||
assert(match, `illegal identifier ${token.args}`)
|
||||
this.variable = match[0]
|
||||
},
|
||||
render: function (scope: Scope) {
|
||||
let context = scope.findContextFor(
|
||||
this.variable,
|
||||
ctx => {
|
||||
return !(ctx instanceof CaptureScope) && !(ctx instanceof AssignScope)
|
||||
}
|
||||
)
|
||||
if (!context) {
|
||||
context = new DecrementScope()
|
||||
scope.unshift(context)
|
||||
render: function (context: Context) {
|
||||
const scope = context.environments
|
||||
if (typeof scope[this.variable] !== 'number') {
|
||||
scope[this.variable] = 0
|
||||
}
|
||||
if (typeof context[this.variable] !== 'number') {
|
||||
context[this.variable] = 0
|
||||
}
|
||||
return --context[this.variable]
|
||||
return --scope[this.variable]
|
||||
}
|
||||
} as ITagImplOptions
|
||||
|
||||
+15
-32
@@ -1,15 +1,15 @@
|
||||
import { mapSeries } from '../../util/promise'
|
||||
import { isString, isObject, isArray } from '../../util/underscore'
|
||||
import { evalExp } 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 Scope from '../../scope/scope'
|
||||
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})` +
|
||||
@@ -41,18 +41,18 @@ export default <ITagImplOptions>{
|
||||
|
||||
stream.start()
|
||||
},
|
||||
render: async function (scope: Scope, hash: Hash) {
|
||||
let collection = evalExp(this.collection, scope)
|
||||
render: async function (ctx: Context, hash: Hash) {
|
||||
let collection = await evalExp(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]]) as Array<[string, any]>
|
||||
collection = Object.keys(collection).map((key) => [key, collection[key]])
|
||||
}
|
||||
}
|
||||
if (!isArray(collection) || !collection.length) {
|
||||
return this.liquid.renderer.renderTemplates(this.elseTemplates, scope)
|
||||
return this.liquid.renderer.renderTemplates(this.elseTemplates, ctx)
|
||||
}
|
||||
|
||||
const offset = hash.offset || 0
|
||||
@@ -61,39 +61,22 @@ export default <ITagImplOptions>{
|
||||
collection = collection.slice(offset, offset + limit)
|
||||
if (this.reversed) collection.reverse()
|
||||
|
||||
const contexts = collection.map((item: string, i: number) => {
|
||||
const ctx = {}
|
||||
ctx[this.variable] = item
|
||||
ctx['forloop'] = {
|
||||
first: i === 0,
|
||||
index: i + 1,
|
||||
index0: i,
|
||||
last: i === collection.length - 1,
|
||||
length: collection.length,
|
||||
rindex: collection.length - i,
|
||||
rindex0: collection.length - i - 1
|
||||
}
|
||||
return ctx
|
||||
})
|
||||
|
||||
const context = { forloop: new ForloopDrop(collection.length) }
|
||||
ctx.push(context)
|
||||
let html = ''
|
||||
let finished = false
|
||||
await mapSeries(contexts, async context => {
|
||||
if (finished) return
|
||||
|
||||
scope.push(context)
|
||||
for (const item of collection) {
|
||||
context[this.variable] = item
|
||||
try {
|
||||
html += await this.liquid.renderer.renderTemplates(this.templates, scope)
|
||||
html += await this.liquid.renderer.renderTemplates(this.templates, ctx)
|
||||
} catch (e) {
|
||||
if (e.name === 'RenderBreakError') {
|
||||
html += e.resolvedHTML
|
||||
if (e.message === 'break') {
|
||||
finished = true
|
||||
}
|
||||
if (e.message === 'break') break
|
||||
} else throw e
|
||||
}
|
||||
scope.pop(context)
|
||||
})
|
||||
context.forloop.next()
|
||||
}
|
||||
ctx.pop()
|
||||
return html
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { evalExp, isTruthy } from '../../render/syntax'
|
||||
import TagToken from '../../parser/tag-token'
|
||||
import Token from '../../parser/token'
|
||||
import Scope from '../../scope/scope'
|
||||
import Context from '../../context/context'
|
||||
import ITemplate from '../../template/itemplate'
|
||||
import ITagImplOptions from '../../template/tag/itag-impl-options'
|
||||
import ParseStream from '../../parser/parse-stream'
|
||||
@@ -33,13 +33,13 @@ export default {
|
||||
stream.start()
|
||||
},
|
||||
|
||||
render: function (scope: Scope) {
|
||||
render: async function (ctx: Context) {
|
||||
for (const branch of this.branches) {
|
||||
const cond = evalExp(branch.cond, scope)
|
||||
const cond = await evalExp(branch.cond, ctx)
|
||||
if (isTruthy(cond)) {
|
||||
return this.liquid.renderer.renderTemplates(branch.templates, scope)
|
||||
return this.liquid.renderer.renderTemplates(branch.templates, ctx)
|
||||
}
|
||||
}
|
||||
return this.liquid.renderer.renderTemplates(this.elseTemplates, scope)
|
||||
return this.liquid.renderer.renderTemplates(this.elseTemplates, ctx)
|
||||
}
|
||||
} as ITagImplOptions
|
||||
|
||||
+17
-17
@@ -1,9 +1,9 @@
|
||||
import assert from '../../util/assert'
|
||||
import { value, quotedLine } from '../../parser/lexical'
|
||||
import { evalValue } from '../../render/syntax'
|
||||
import BlockMode from '../../scope/block-mode'
|
||||
import BlockMode from '../../context/block-mode'
|
||||
import TagToken from '../../parser/tag-token'
|
||||
import Scope from '../../scope/scope'
|
||||
import Context from '../../context/context'
|
||||
import Hash from '../../template/tag/hash'
|
||||
import ITagImplOptions from '../../template/tag/itag-impl-options'
|
||||
|
||||
@@ -27,34 +27,34 @@ export default <ITagImplOptions>{
|
||||
this.with = match[1]
|
||||
}
|
||||
},
|
||||
render: async function (scope: Scope, hash: Hash) {
|
||||
render: async function (ctx: Context, hash: Hash) {
|
||||
let filepath
|
||||
if (scope.opts.dynamicPartials) {
|
||||
if (ctx.opts.dynamicPartials) {
|
||||
if (quotedLine.exec(this.value)) {
|
||||
const template = this.value.slice(1, -1)
|
||||
filepath = await this.liquid.parseAndRender(template, scope.getAll(), scope.opts)
|
||||
filepath = await this.liquid.parseAndRender(template, ctx.getAll(), ctx.opts)
|
||||
} else {
|
||||
filepath = evalValue(this.value, scope)
|
||||
filepath = await evalValue(this.value, ctx)
|
||||
}
|
||||
} else {
|
||||
filepath = this.staticValue
|
||||
}
|
||||
assert(filepath, `cannot include with empty filename`)
|
||||
|
||||
const originBlocks = scope.blocks
|
||||
const originBlockMode = scope.blockMode
|
||||
const originBlocks = ctx.getRegister('blocks')
|
||||
const originBlockMode = ctx.getRegister('blockMode')
|
||||
|
||||
scope.blocks = {}
|
||||
scope.blockMode = BlockMode.OUTPUT
|
||||
ctx.setRegister('blocks', {})
|
||||
ctx.setRegister('blockMode', BlockMode.OUTPUT)
|
||||
if (this.with) {
|
||||
hash[filepath] = evalValue(this.with, scope)
|
||||
hash[filepath] = await evalValue(this.with, ctx)
|
||||
}
|
||||
const templates = await this.liquid.getTemplate(filepath, scope.opts)
|
||||
scope.push(hash)
|
||||
const html = await this.liquid.renderer.renderTemplates(templates, scope)
|
||||
scope.pop(hash)
|
||||
scope.blocks = originBlocks
|
||||
scope.blockMode = originBlockMode
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import assert from '../../util/assert'
|
||||
import { identifier } from '../../parser/lexical'
|
||||
import { CaptureScope, AssignScope, IncrementScope } from '../../scope/scopes'
|
||||
import ITagImplOptions from '../../template/tag/itag-impl-options'
|
||||
|
||||
export default {
|
||||
@@ -9,22 +8,13 @@ export default {
|
||||
assert(match, `illegal identifier ${token.args}`)
|
||||
this.variable = match![0]
|
||||
},
|
||||
render: function (scope) {
|
||||
let context = scope.findContextFor(
|
||||
this.variable,
|
||||
ctx => {
|
||||
return !(ctx instanceof CaptureScope) && !(ctx instanceof AssignScope)
|
||||
}
|
||||
)
|
||||
if (!context) {
|
||||
context = new IncrementScope()
|
||||
scope.unshift(context)
|
||||
render: function (context) {
|
||||
const scope = context.environments
|
||||
if (typeof scope[this.variable] !== 'number') {
|
||||
scope[this.variable] = 0
|
||||
}
|
||||
if (typeof context[this.variable] !== 'number') {
|
||||
context[this.variable] = 0
|
||||
}
|
||||
const val = context[this.variable]
|
||||
context[this.variable]++
|
||||
const val = scope[this.variable]
|
||||
scope[this.variable]++
|
||||
return val
|
||||
}
|
||||
} as ITagImplOptions
|
||||
|
||||
+15
-14
@@ -1,10 +1,10 @@
|
||||
import assert from '../../util/assert'
|
||||
import { value as rValue } from '../../parser/lexical'
|
||||
import { evalValue } from '../../render/syntax'
|
||||
import BlockMode from '../../scope/block-mode'
|
||||
import BlockMode from '../../context/block-mode'
|
||||
import TagToken from '../../parser/tag-token'
|
||||
import Token from '../../parser/token'
|
||||
import Scope from '../../scope/scope'
|
||||
import Context from '../../context/context'
|
||||
import Hash from '../../template/tag/hash'
|
||||
import ITagImplOptions from '../../template/tag/itag-impl-options'
|
||||
|
||||
@@ -24,23 +24,24 @@ export default {
|
||||
|
||||
this.tpls = this.liquid.parser.parse(remainTokens)
|
||||
},
|
||||
render: async function (scope: Scope, hash: Hash) {
|
||||
const layout = scope.opts.dynamicPartials
|
||||
? evalValue(this.layout, scope)
|
||||
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
|
||||
scope.blockMode = BlockMode.STORE
|
||||
const html = await this.liquid.renderer.renderTemplates(this.tpls, scope)
|
||||
if (scope.blocks[''] === undefined) {
|
||||
scope.blocks[''] = html
|
||||
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, scope.opts)
|
||||
scope.push(hash)
|
||||
scope.blockMode = BlockMode.OUTPUT
|
||||
const partial = await this.liquid.renderer.renderTemplates(templates, scope)
|
||||
scope.pop(hash)
|
||||
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
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { mapSeries } from '../../util/promise'
|
||||
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 Scope from '../../scope/scope'
|
||||
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})` +
|
||||
@@ -35,41 +35,31 @@ export default {
|
||||
stream.start()
|
||||
},
|
||||
|
||||
render: async function (scope: Scope, hash: Hash) {
|
||||
let collection = evalExp(this.collection, scope) || []
|
||||
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 contexts = collection.map((item: any) => {
|
||||
const ctx = {}
|
||||
ctx[this.variable] = item
|
||||
return ctx
|
||||
})
|
||||
|
||||
let row: number = 0
|
||||
const tablerowloop = new TablerowloopDrop(collection.length, cols)
|
||||
const scope = { tablerowloop }
|
||||
ctx.push(scope)
|
||||
|
||||
let html = ''
|
||||
await mapSeries(contexts, async (context, idx) => {
|
||||
row = Math.floor(idx / cols) + 1
|
||||
const col = (idx % cols) + 1
|
||||
if (col === 1) {
|
||||
if (row !== 1) {
|
||||
html += '</tr>'
|
||||
}
|
||||
html += `<tr class="row${row}">`
|
||||
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${col}">`
|
||||
scope.push(context)
|
||||
html += await this.liquid.renderer.renderTemplates(this.templates, scope)
|
||||
html += `<td class="col${tablerowloop.col()}">`
|
||||
html += await this.liquid.renderer.renderTemplates(this.templates, ctx)
|
||||
html += '</td>'
|
||||
scope.pop(context)
|
||||
return html
|
||||
})
|
||||
if (row > 0) {
|
||||
html += '</tr>'
|
||||
}
|
||||
if (collection.length) html += '</tr>'
|
||||
ctx.pop()
|
||||
return html
|
||||
}
|
||||
} as ITagImplOptions
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { evalExp, isFalsy } from '../../render/syntax'
|
||||
import TagToken from '../../parser/tag-token'
|
||||
import Token from '../../parser/token'
|
||||
import Scope from '../../scope/scope'
|
||||
import Context from '../../context/context'
|
||||
import ITagImplOptions from '../../template/tag/itag-impl-options'
|
||||
import ParseStream from '../../parser/parse-stream'
|
||||
|
||||
@@ -25,10 +25,10 @@ export default {
|
||||
stream.start()
|
||||
},
|
||||
|
||||
render: function (scope: Scope) {
|
||||
const cond = evalExp(this.cond, scope)
|
||||
render: async function (ctx: Context) {
|
||||
const cond = await evalExp(this.cond, ctx)
|
||||
return isFalsy(cond)
|
||||
? this.liquid.renderer.renderTemplates(this.templates, scope)
|
||||
: this.liquid.renderer.renderTemplates(this.elseTemplates, scope)
|
||||
? this.liquid.renderer.renderTemplates(this.templates, ctx)
|
||||
: this.liquid.renderer.renderTemplates(this.elseTemplates, ctx)
|
||||
}
|
||||
} as ITagImplOptions
|
||||
|
||||
@@ -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
|
||||
+1
-1
@@ -3,7 +3,7 @@ export abstract class Drop {
|
||||
return undefined
|
||||
}
|
||||
|
||||
liquidMethodMissing (key: string): string | undefined {
|
||||
liquidMethodMissing (key: string): Promise<string | undefined> | string | undefined {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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,4 +1,3 @@
|
||||
import { deprecate } from './util/deprecate'
|
||||
import * as _ from './util/underscore'
|
||||
|
||||
export interface LiquidOptions {
|
||||
@@ -77,13 +76,6 @@ export function normalize (options?: LiquidOptions): NormalizedOptions {
|
||||
if (options.hasOwnProperty('root')) {
|
||||
options.root = normalizeStringArray(options.root)
|
||||
}
|
||||
for (const key of Object.keys(options)) {
|
||||
if (key.indexOf('_') > -1) {
|
||||
const newKey = key.replace(/_([a-z])/g, (_, ch) => ch.toUpperCase())
|
||||
deprecate(`${key} is deprecated, use ${newKey} instead.`, 109)
|
||||
options[newKey] = options[key]
|
||||
}
|
||||
}
|
||||
return options as NormalizedOptions
|
||||
}
|
||||
|
||||
|
||||
+7
-7
@@ -1,4 +1,4 @@
|
||||
import Scope from './scope/scope'
|
||||
import Context from './context/context'
|
||||
import * as Types from './types'
|
||||
import fs from './fs/node'
|
||||
import * as _ from './util/underscore'
|
||||
@@ -6,7 +6,7 @@ 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 { Filter } from './template/filter/filter'
|
||||
import Parser from './parser/parser'
|
||||
import ITagImplOptions from './template/tag/itag-impl-options'
|
||||
import Value from './template/value'
|
||||
@@ -14,7 +14,7 @@ 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 { FilterImpl } from './template/filter/filter-impl'
|
||||
import { FilterImplOptions } from './template/filter/filter-impl-options'
|
||||
|
||||
export default class Liquid {
|
||||
public options: NormalizedFullOptions
|
||||
@@ -38,7 +38,7 @@ export default class Liquid {
|
||||
}
|
||||
render (tpl: Array<ITemplate>, ctx?: object, opts?: LiquidOptions) {
|
||||
const options = { ...this.options, ...normalize(opts) }
|
||||
const scope = new Scope(ctx, options)
|
||||
const scope = new Context(ctx, options)
|
||||
return this.renderer.renderTemplates(tpl, scope)
|
||||
}
|
||||
async parseAndRender (html: string, ctx?: object, opts?: LiquidOptions) {
|
||||
@@ -69,10 +69,10 @@ export default class Liquid {
|
||||
const templates = await this.getTemplate(file, options)
|
||||
return this.render(templates, ctx, opts)
|
||||
}
|
||||
evalValue (str: string, scope: Scope) {
|
||||
return new Value(str, this.options.strictFilters).value(scope)
|
||||
evalValue (str: string, ctx: Context) {
|
||||
return new Value(str, this.options.strictFilters).value(ctx)
|
||||
}
|
||||
registerFilter (name: string, filter: FilterImpl) {
|
||||
registerFilter (name: string, filter: FilterImplOptions) {
|
||||
return Filter.register(name, filter)
|
||||
}
|
||||
registerTag (name: string, tag: ITagImplOptions) {
|
||||
|
||||
@@ -2,17 +2,26 @@ import Token from './token'
|
||||
import { last } from '../util/underscore'
|
||||
|
||||
export default class DelimitedToken extends Token {
|
||||
trimLeft: boolean
|
||||
trimRight: boolean
|
||||
constructor (raw: string, value: string, input: string, line: number, pos: number, file?: string) {
|
||||
constructor (
|
||||
raw: string,
|
||||
value: string,
|
||||
input: string,
|
||||
line: number,
|
||||
pos: number,
|
||||
trimLeft: boolean,
|
||||
trimRight: boolean,
|
||||
file?: string
|
||||
) {
|
||||
super(raw, input, line, pos, file)
|
||||
this.trimLeft = value[0] === '-'
|
||||
this.trimRight = last(value) === '-'
|
||||
const tl = value[0] === '-'
|
||||
const tr = last(value) === '-'
|
||||
this.value = value
|
||||
.slice(
|
||||
this.trimLeft ? 1 : 0,
|
||||
this.trimRight ? -1 : value.length
|
||||
tl ? 1 : 0,
|
||||
tr ? -1 : value.length
|
||||
)
|
||||
.trim()
|
||||
this.trimLeft = tl || trimLeft
|
||||
this.trimRight = tr || trimRight
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,4 +6,7 @@ export default class HTMLToken extends Token {
|
||||
this.type = 'html'
|
||||
this.value = str
|
||||
}
|
||||
static is (token: Token): token is HTMLToken {
|
||||
return token.type === 'html'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +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, file?: string) {
|
||||
super(raw, value, input, line, pos, file)
|
||||
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'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,20 +18,16 @@ export default class ParseStream {
|
||||
this.handlers[name] = cb
|
||||
return this
|
||||
}
|
||||
trigger <T extends Token | ITemplate> (event: string, arg?: T) {
|
||||
private trigger <T extends Token | ITemplate> (event: string, arg?: T) {
|
||||
const h = this.handlers[event]
|
||||
if (typeof h === 'function') {
|
||||
h(arg)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
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 (token.type === 'tag' && this.trigger(`tag:${(<TagToken>token).name}`, token)) {
|
||||
if (TagToken.is(token) && this.trigger(`tag:${token.name}`, token)) {
|
||||
continue
|
||||
}
|
||||
const template = this.parseToken(token, this.tokens)
|
||||
|
||||
@@ -25,10 +25,10 @@ export default class Parser {
|
||||
}
|
||||
parseToken (token: Token, remainTokens: Array<Token>) {
|
||||
try {
|
||||
if (token.type === 'tag') {
|
||||
return new Tag(token as TagToken, remainTokens, this.liquid)
|
||||
if (TagToken.is(token)) {
|
||||
return new Tag(token, remainTokens, this.liquid)
|
||||
}
|
||||
if (token.type === 'output') {
|
||||
if (OutputToken.is(token)) {
|
||||
return new Output(token as OutputToken, this.liquid.options.strictFilters)
|
||||
}
|
||||
return new HTML(token)
|
||||
|
||||
+15
-2
@@ -1,12 +1,22 @@
|
||||
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, file?: string) {
|
||||
super(raw, value, input, line, pos, file)
|
||||
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) {
|
||||
@@ -15,4 +25,7 @@ export default class TagToken extends DelimitedToken {
|
||||
this.name = match[1]
|
||||
this.args = match[2]
|
||||
}
|
||||
static is (token: Token): token is TagToken {
|
||||
return token.type === 'tag'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export default class Token {
|
||||
trimLeft: boolean = false
|
||||
trimRight: boolean = false
|
||||
type: string = 'notset'
|
||||
line: number
|
||||
col: number
|
||||
|
||||
+24
-19
@@ -9,16 +9,18 @@ import { NormalizedFullOptions, applyDefault } from '../liquid-options'
|
||||
enum ParseState { HTML, OUTPUT, TAG }
|
||||
|
||||
export default class Tokenizer {
|
||||
options: NormalizedFullOptions
|
||||
private options: NormalizedFullOptions
|
||||
constructor (options?: NormalizedFullOptions) {
|
||||
this.options = applyDefault(options)
|
||||
}
|
||||
tokenize (input: string, file?: string) {
|
||||
const tokens: Token[] = []
|
||||
const tagL = this.options.tagDelimiterLeft
|
||||
const tagR = this.options.tagDelimiterRight
|
||||
const outputL = this.options.outputDelimiterLeft
|
||||
const outputR = this.options.outputDelimiterRight
|
||||
const {
|
||||
tagDelimiterLeft,
|
||||
tagDelimiterRight,
|
||||
outputDelimiterLeft,
|
||||
outputDelimiterRight
|
||||
} = this.options
|
||||
let p = 0
|
||||
let curLine = 1
|
||||
let state = ParseState.HTML
|
||||
@@ -33,36 +35,39 @@ export default class Tokenizer {
|
||||
lineBegin = p + 1
|
||||
}
|
||||
if (state === ParseState.HTML) {
|
||||
if (input.substr(p, outputL.length) === outputL) {
|
||||
if (input.substr(p, outputDelimiterLeft.length) === outputDelimiterLeft) {
|
||||
if (buffer) tokens.push(new HTMLToken(buffer, input, line, col, file))
|
||||
buffer = outputL
|
||||
buffer = outputDelimiterLeft
|
||||
line = curLine
|
||||
col = p - lineBegin + 1
|
||||
p += outputL.length
|
||||
p += outputDelimiterLeft.length
|
||||
state = ParseState.OUTPUT
|
||||
continue
|
||||
} else if (input.substr(p, tagL.length) === tagL) {
|
||||
} else if (input.substr(p, tagDelimiterLeft.length) === tagDelimiterLeft) {
|
||||
if (buffer) tokens.push(new HTMLToken(buffer, input, line, col, file))
|
||||
buffer = tagL
|
||||
buffer = tagDelimiterLeft
|
||||
line = curLine
|
||||
col = p - lineBegin + 1
|
||||
p += tagL.length
|
||||
p += tagDelimiterLeft.length
|
||||
state = ParseState.TAG
|
||||
continue
|
||||
}
|
||||
} else if (state === ParseState.OUTPUT && input.substr(p, outputR.length) === outputR) {
|
||||
buffer += outputR
|
||||
tokens.push(new OutputToken(buffer, buffer.slice(outputL.length, -outputR.length), input, line, col, file))
|
||||
p += outputR.length
|
||||
} 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, tagR.length) === tagR) {
|
||||
buffer += tagR
|
||||
tokens.push(new TagToken(buffer, buffer.slice(tagL.length, -tagR.length), input, line, col, file))
|
||||
p += tagR.length
|
||||
} 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
|
||||
|
||||
@@ -1,47 +1,38 @@
|
||||
import DelimitedToken from '../parser/delimited-token'
|
||||
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
|
||||
|
||||
tokens.forEach((token: Token, i: number) => {
|
||||
if (shouldTrimLeft(token as DelimitedToken, inRaw, options)) {
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
const token = tokens[i]
|
||||
if (!inRaw && token.trimLeft) {
|
||||
trimLeft(tokens[i - 1], options.greedy)
|
||||
}
|
||||
|
||||
if (token.type === 'tag' && (token as TagToken).name === 'raw') inRaw = true
|
||||
if (token.type === 'tag' && (token as TagToken).name === 'endraw') inRaw = false
|
||||
if (TagToken.is(token)) {
|
||||
if (token.name === 'raw') inRaw = true
|
||||
else if (token.name === 'endraw') inRaw = false
|
||||
}
|
||||
|
||||
if (shouldTrimRight(token as DelimitedToken, inRaw, options)) {
|
||||
if (!inRaw && token.trimRight) {
|
||||
trimRight(tokens[i + 1], options.greedy)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function shouldTrimLeft (token: DelimitedToken, inRaw: boolean, options: NormalizedFullOptions) {
|
||||
if (inRaw) return false
|
||||
if (token.type === 'tag') return token.trimLeft || options.trimTagLeft
|
||||
if (token.type === 'output') return token.trimLeft || options.trimOutputLeft
|
||||
}
|
||||
|
||||
function shouldTrimRight (token: DelimitedToken, inRaw: boolean, options: NormalizedFullOptions) {
|
||||
if (inRaw) return false
|
||||
if (token.type === 'tag') return token.trimRight || options.trimTagRight
|
||||
if (token.type === 'output') return token.trimRight || options.trimOutputRight
|
||||
}
|
||||
}
|
||||
|
||||
function trimLeft (token: Token, greedy: boolean) {
|
||||
if (!token || token.type !== 'html') return
|
||||
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 || token.type !== 'html') return
|
||||
if (!token || !HTMLToken.is(token)) return
|
||||
|
||||
const rRight = greedy ? /^\s+/g : /^[\t\r ]*\n?/g
|
||||
token.value = token.value.replace(rRight, '')
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { RenderError } from '../util/error'
|
||||
import assert from '../util/assert'
|
||||
import Scope from '../scope/scope'
|
||||
import Context from '../context/context'
|
||||
import ITemplate from '../template/itemplate'
|
||||
|
||||
export default class Render {
|
||||
async renderTemplates (templates: ITemplate[], scope: Scope) {
|
||||
assert(scope, 'unable to evalTemplates: scope undefined')
|
||||
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(scope)
|
||||
html += await tpl.render(ctx)
|
||||
} catch (e) {
|
||||
if (e.name === 'RenderBreakError') {
|
||||
e.resolvedHTML = html
|
||||
|
||||
+17
-19
@@ -1,7 +1,7 @@
|
||||
import * as lexical from '../parser/lexical'
|
||||
import assert from '../util/assert'
|
||||
import Scope from '../scope/scope'
|
||||
import { range, last } from '../util/underscore'
|
||||
import Context from '../context/context'
|
||||
import { range, last, isFunction } from '../util/underscore'
|
||||
import { isComparable } from '../drop/icomparable'
|
||||
import { NullDrop } from '../drop/null-drop'
|
||||
import { EmptyDrop } from '../drop/empty-drop'
|
||||
@@ -40,44 +40,42 @@ const binaryOperators: {[key: string]: (lhs: any, rhs: any) => boolean} = {
|
||||
return l <= r
|
||||
},
|
||||
'contains': (l: any, r: any) => {
|
||||
if (!l) return false
|
||||
if (typeof l.indexOf !== 'function') return false
|
||||
return l.indexOf(r) > -1
|
||||
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 function parseExp (exp: string, scope: Scope): any {
|
||||
assert(scope, 'unable to parseExp: scope undefined')
|
||||
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 = parseExp(match[1], scope)
|
||||
const l = await parseExp(match[1], ctx)
|
||||
const op = binaryOperators[match[2].trim()]
|
||||
const r = parseExp(match[3], scope)
|
||||
const r = await parseExp(match[3], ctx)
|
||||
return op(l, r)
|
||||
}
|
||||
}
|
||||
|
||||
if ((match = exp.match(lexical.rangeLine))) {
|
||||
const low = evalValue(match[1], scope)
|
||||
const high = evalValue(match[2], scope)
|
||||
return range(low, high + 1)
|
||||
const low = await evalValue(match[1], ctx)
|
||||
const high = await evalValue(match[2], ctx)
|
||||
return range(+low, +high + 1)
|
||||
}
|
||||
|
||||
return parseValue(exp, scope)
|
||||
return parseValue(exp, ctx)
|
||||
}
|
||||
|
||||
export function evalExp (str: string, scope: Scope): any {
|
||||
const value = parseExp(str, scope)
|
||||
export async function evalExp (str: string, ctx: Context): Promise<any> {
|
||||
const value = await parseExp(str, ctx)
|
||||
return value instanceof Drop ? value.valueOf() : value
|
||||
}
|
||||
|
||||
function parseValue (str: string, scope: Scope): any {
|
||||
async function parseValue (str: string | undefined, ctx: Context): Promise<any> {
|
||||
if (!str) return null
|
||||
str = str.trim()
|
||||
|
||||
@@ -88,11 +86,11 @@ function parseValue (str: string, scope: Scope): any {
|
||||
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 scope.get(str)
|
||||
return ctx.get(str)
|
||||
}
|
||||
|
||||
export function evalValue (str: string, scope: Scope): any {
|
||||
const value = parseValue(str, scope)
|
||||
export async function evalValue (str: string | undefined, ctx: Context) {
|
||||
const value = await parseValue(str, ctx)
|
||||
return value instanceof Drop ? value.valueOf() : value
|
||||
}
|
||||
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
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 BlockMode from './block-mode'
|
||||
|
||||
export type Context = {
|
||||
[key: string]: any
|
||||
liquid_method_missing?: (key: string) => any // eslint-disable-line
|
||||
to_liquid?: () => any // eslint-disable-line
|
||||
toLiquid?: () => any // eslint-disable-line
|
||||
}
|
||||
|
||||
export default class Scope {
|
||||
opts: NormalizedFullOptions
|
||||
contexts: Array<Context>
|
||||
blocks: object = {}
|
||||
groups: {[key: string]: number} = {}
|
||||
blockMode: BlockMode = BlockMode.OUTPUT
|
||||
constructor (ctx: object = {}, opts?: NormalizedFullOptions) {
|
||||
this.opts = applyDefault(opts)
|
||||
this.contexts = [ctx || {}]
|
||||
}
|
||||
getAll () {
|
||||
return this.contexts.reduce((ctx, val) => __assign(ctx, val), {})
|
||||
}
|
||||
get (path: string): any {
|
||||
const paths = this.propertyAccessSeq(path)
|
||||
const scope = this.findContextFor(paths[0]) || _.last(this.contexts)
|
||||
return paths.reduce((value, key) => {
|
||||
const val = this.readProperty(value, key)
|
||||
if (_.isNil(val) && this.opts.strictVariables) {
|
||||
throw new TypeError(`undefined variable: ${key}`)
|
||||
}
|
||||
return val
|
||||
}, scope)
|
||||
}
|
||||
set (path: string, v: any): void {
|
||||
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]
|
||||
return false
|
||||
})
|
||||
}
|
||||
unshift (ctx: object) {
|
||||
return this.contexts.unshift(ctx)
|
||||
}
|
||||
push (ctx: object) {
|
||||
return this.contexts.push(ctx)
|
||||
}
|
||||
pop (ctx?: object): object | undefined {
|
||||
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 (key: string, filter: ((conttext: object) => boolean) = () => 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
|
||||
}
|
||||
private readProperty (obj: Context, 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]
|
||||
}
|
||||
|
||||
/*
|
||||
* 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 (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(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 readSize (obj: Context) {
|
||||
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
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export class CaptureScope {}
|
||||
export class AssignScope {}
|
||||
export class IncrementScope {}
|
||||
export class DecrementScope {}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { FilterImpl } from './filter-impl'
|
||||
|
||||
export type FilterImplOptions = (this: FilterImpl, value: any, ...args: any[]) => any
|
||||
@@ -1 +1,5 @@
|
||||
export type FilterImpl = (value: any, ...args: any[]) => any
|
||||
import Context from '../../context/context'
|
||||
|
||||
export interface FilterImpl {
|
||||
context: Context
|
||||
}
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { evalValue } from '../../render/syntax'
|
||||
import Scope from '../../scope/scope'
|
||||
import { FilterImpl } from './filter-impl'
|
||||
import Context from '../../context/context'
|
||||
import { isArray } from '../../util/underscore'
|
||||
import { FilterImplOptions } from './filter-impl-options'
|
||||
|
||||
export default class Filter {
|
||||
export type FilterArgs = Array<string|[string?, string?]>
|
||||
|
||||
export class Filter {
|
||||
name: string
|
||||
impl: FilterImpl
|
||||
args: string[]
|
||||
private static impls: {[key: string]: FilterImpl} = {}
|
||||
impl: FilterImplOptions
|
||||
args: FilterArgs
|
||||
private static impls: {[key: string]: FilterImplOptions} = {}
|
||||
|
||||
constructor (name: string, args: string[], strictFilters: boolean) {
|
||||
constructor (name: string, args: FilterArgs, strictFilters: boolean) {
|
||||
const impl = Filter.impls[name]
|
||||
if (!impl && strictFilters) throw new TypeError(`undefined filter: ${name}`)
|
||||
|
||||
@@ -16,11 +19,15 @@ export default class Filter {
|
||||
this.impl = impl || (x => x)
|
||||
this.args = args
|
||||
}
|
||||
render (value: any, scope: Scope): any {
|
||||
const args = this.args.map(arg => evalValue(arg, scope))
|
||||
return this.impl.apply(null, [value, ...args])
|
||||
async render (value: any, context: Context) {
|
||||
const argv: any[] = []
|
||||
for (const arg of this.args) {
|
||||
if (isArray(arg)) argv.push([arg[0], await evalValue(arg[1], context)])
|
||||
else argv.push(await evalValue(arg, context))
|
||||
}
|
||||
return this.impl.apply({ context }, [value, ...argv])
|
||||
}
|
||||
static register (name: string, filter: FilterImpl) {
|
||||
static register (name: string, filter: FilterImplOptions) {
|
||||
Filter.impls[name] = filter
|
||||
}
|
||||
static clear () {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Scope from '../scope/scope'
|
||||
import Context from '../context/context'
|
||||
import Token from '../parser/token'
|
||||
|
||||
export default interface ITemplate {
|
||||
token: Token;
|
||||
render(scope: Scope): Promise<string>;
|
||||
render(ctx: Context): Promise<string>;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import Value from './value'
|
||||
import { stringify } from '../util/underscore'
|
||||
import Template from '../template/template'
|
||||
import ITemplate from '../template/itemplate'
|
||||
import Scope from '../scope/scope'
|
||||
import Context from '../context/context'
|
||||
import OutputToken from '../parser/output-token'
|
||||
|
||||
export default class Output extends Template<OutputToken> implements ITemplate {
|
||||
@@ -11,8 +11,8 @@ export default class Output extends Template<OutputToken> implements ITemplate {
|
||||
super(token)
|
||||
this.value = new Value(token.value, strictFilters)
|
||||
}
|
||||
async render (scope: Scope): Promise<string> {
|
||||
const html = await this.value.value(scope)
|
||||
async render (ctx: Context): Promise<string> {
|
||||
const html = await this.value.value(ctx)
|
||||
return stringify(html)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { hashCapture } from '../../parser/lexical'
|
||||
import { evalValue } from '../../render/syntax'
|
||||
import Scope from '../../scope/scope'
|
||||
import Context from '../../context/context'
|
||||
|
||||
/**
|
||||
* Key-Value Pairs Representing Tag Arguments
|
||||
@@ -10,13 +10,15 @@ import Scope from '../../scope/scope'
|
||||
*/
|
||||
export default class Hash {
|
||||
[key: string]: any
|
||||
constructor (markup: string, scope: Scope) {
|
||||
static async create (markup: string, ctx: Context) {
|
||||
const instance = new Hash()
|
||||
let match
|
||||
hashCapture.lastIndex = 0
|
||||
while ((match = hashCapture.exec(markup))) {
|
||||
const k = match[1]
|
||||
const v = match[2]
|
||||
this[k] = evalValue(v, scope)
|
||||
instance[k] = await evalValue(v, ctx)
|
||||
}
|
||||
return instance
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Scope from '../../scope/scope'
|
||||
import Context from '../../context/context'
|
||||
import TagToken from '../../parser/tag-token'
|
||||
import Token from '../../parser/token'
|
||||
import Hash from '../../template/tag/hash'
|
||||
@@ -6,5 +6,5 @@ import ITagImpl from './itag-impl'
|
||||
|
||||
export default interface ITagImplOptions {
|
||||
parse?: (this: ITagImpl, token: TagToken, remainingTokens: Array<Token>) => void
|
||||
render?: (this: ITagImpl, scope: Scope, hash: Hash) => any | Promise<any>
|
||||
render?: (this: ITagImpl, ctx: Context, hash: Hash) => any | Promise<any>
|
||||
}
|
||||
|
||||
+7
-10
@@ -1,6 +1,6 @@
|
||||
import { create, stringify } from '../../util/underscore'
|
||||
import { stringify, isFunction } from '../../util/underscore'
|
||||
import assert from '../../util/assert'
|
||||
import Scope from '../../scope/scope'
|
||||
import Context from '../../context/context'
|
||||
import ITagImpl from './itag-impl'
|
||||
import ITagImplOptions from './itag-impl-options'
|
||||
import Liquid from '../../liquid'
|
||||
@@ -21,20 +21,17 @@ export default class Tag extends Template<TagToken> implements ITemplate {
|
||||
|
||||
const impl = Tag.impls[token.name]
|
||||
assert(impl, `tag ${token.name} not found`)
|
||||
this.impl = create<ITagImplOptions, ITagImpl>(impl)
|
||||
|
||||
this.impl = Object.create(impl)
|
||||
this.impl.liquid = liquid
|
||||
if (this.impl.parse) {
|
||||
this.impl.parse(token, tokens)
|
||||
}
|
||||
}
|
||||
async render (scope: Scope) {
|
||||
const hash = new Hash(this.token.args, scope)
|
||||
async render (ctx: Context) {
|
||||
const hash = await Hash.create(this.token.args, ctx)
|
||||
const impl = this.impl
|
||||
if (typeof impl.render !== 'function') {
|
||||
return ''
|
||||
}
|
||||
const html = await impl.render(scope, hash)
|
||||
return stringify(html)
|
||||
return isFunction(impl.render) ? stringify(await impl.render(ctx, hash)) : ''
|
||||
}
|
||||
static register (name: string, tag: ITagImplOptions) {
|
||||
Tag.impls[name] = tag
|
||||
|
||||
+66
-68
@@ -1,80 +1,78 @@
|
||||
import { evalExp } from '../render/syntax'
|
||||
import Filter from './filter/filter'
|
||||
import Scope from '../scope/scope'
|
||||
import { FilterArgs, Filter } from './filter/filter'
|
||||
import Context from '../context/context'
|
||||
|
||||
enum ParseState {
|
||||
INIT = 0,
|
||||
FILTER_NAME = 1,
|
||||
FILTER_ARG = 2
|
||||
}
|
||||
|
||||
export default class {
|
||||
initial: any
|
||||
filters: Array<Filter> = []
|
||||
export default class Value {
|
||||
private strictFilters: boolean
|
||||
private initial: string
|
||||
private filters: Array<Filter> = []
|
||||
|
||||
/**
|
||||
* @param str value string, like: "i have a dream | truncate: 3
|
||||
*/
|
||||
constructor (str: string, strictFilters: boolean) {
|
||||
let buffer = ''
|
||||
let quoted = ''
|
||||
let state = ParseState.INIT
|
||||
let sealed = false
|
||||
|
||||
let filterName = ''
|
||||
let filterArgs: string[] = []
|
||||
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
if (quoted) {
|
||||
if (str[i] === quoted) {
|
||||
quoted = ''
|
||||
sealed = true
|
||||
}
|
||||
buffer += str[i]
|
||||
} else if (/\s/.test(str[i])) {
|
||||
if (!buffer) continue
|
||||
else sealed = true
|
||||
} else if (str[i] === '|') {
|
||||
if (state === ParseState.INIT) {
|
||||
this.initial = buffer
|
||||
} else {
|
||||
if (state === ParseState.FILTER_NAME) filterName = buffer
|
||||
else filterArgs.push(buffer)
|
||||
this.filters.push(new Filter(filterName, filterArgs, strictFilters))
|
||||
filterName = ''
|
||||
filterArgs = []
|
||||
}
|
||||
state = ParseState.FILTER_NAME
|
||||
buffer = ''
|
||||
sealed = false
|
||||
} else if (state === ParseState.FILTER_NAME && str[i] === ':') {
|
||||
filterName = buffer
|
||||
state = ParseState.FILTER_ARG
|
||||
buffer = ''
|
||||
sealed = false
|
||||
} else if (state === ParseState.FILTER_ARG && str[i] === ',') {
|
||||
filterArgs.push(buffer)
|
||||
buffer = ''
|
||||
sealed = false
|
||||
} else if (sealed) continue
|
||||
else {
|
||||
if ((str[i] === '"' || str[i] === "'") && !quoted) quoted = str[i]
|
||||
buffer += str[i]
|
||||
}
|
||||
}
|
||||
|
||||
if (buffer) {
|
||||
if (state === ParseState.INIT) this.initial = buffer
|
||||
else if (state === ParseState.FILTER_NAME) this.filters.push(new Filter(buffer, [], strictFilters))
|
||||
else {
|
||||
filterArgs.push(buffer)
|
||||
this.filters.push(new Filter(filterName, filterArgs, strictFilters))
|
||||
const tokens = Value.tokenize(str)
|
||||
this.strictFilters = strictFilters
|
||||
this.initial = tokens[0]
|
||||
this.parseFilters(tokens, 1)
|
||||
}
|
||||
private parseFilters (tokens: string[], begin: number) {
|
||||
let i = begin
|
||||
while (i < tokens.length) {
|
||||
if (tokens[i] !== '|') {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
const j = ++i
|
||||
while (i < tokens.length && tokens[i] !== '|') i++
|
||||
this.parseFilter(tokens, j, i)
|
||||
}
|
||||
}
|
||||
value (scope: Scope) {
|
||||
return this.filters.reduce(
|
||||
(prev, filter) => filter.render(prev, scope),
|
||||
evalExp(this.initial, scope))
|
||||
private parseFilter (tokens: string[], begin: number, end: number) {
|
||||
const name = tokens[begin]
|
||||
const args: FilterArgs = []
|
||||
let argName, argValue
|
||||
for (let i = begin + 1; i < end + 1; i++) {
|
||||
if (i === end || tokens[i] === ',') {
|
||||
if (argName || argValue) {
|
||||
args.push(argName ? [argName, argValue] : <string>argValue)
|
||||
}
|
||||
argValue = argName = undefined
|
||||
} else if (tokens[i] === ':') {
|
||||
argName = argValue
|
||||
argValue = undefined
|
||||
} else if (argValue === undefined) {
|
||||
argValue = tokens[i]
|
||||
}
|
||||
}
|
||||
this.filters.push(new Filter(name, args, this.strictFilters))
|
||||
}
|
||||
async value (ctx: Context) {
|
||||
let val = await evalExp(this.initial, ctx)
|
||||
for (const filter of this.filters) {
|
||||
val = await filter.render(val, ctx)
|
||||
}
|
||||
return val
|
||||
}
|
||||
static tokenize (str: string): Array<'|' | ',' | ':' | string> {
|
||||
const tokens = []
|
||||
let i = 0
|
||||
while (i < str.length) {
|
||||
const ch = str[i]
|
||||
if (ch === '"' || ch === "'") {
|
||||
const j = i
|
||||
for (i += 2; i < str.length && str[i - 1] !== ch; ++i);
|
||||
tokens.push(str.slice(j, i))
|
||||
} else if (/\s/.test(ch)) {
|
||||
i++
|
||||
} else if (/[|,:]/.test(ch)) {
|
||||
tokens.push(str[i++])
|
||||
} else {
|
||||
const j = i++
|
||||
for (; i < str.length && !/[|,:\s]/.test(str[i]); ++i);
|
||||
tokens.push(str.slice(j, i))
|
||||
}
|
||||
}
|
||||
return tokens
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
export { AssignScope, CaptureScope, IncrementScope, DecrementScope } from './scope/scopes'
|
||||
export { ParseError, TokenizationError, RenderBreakError, AssertionError } from './util/error'
|
||||
export { Drop } from './drop/drop'
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { AssertionError } from './error'
|
||||
|
||||
export default function (predicate: any, message?: string) {
|
||||
export default function<T> (predicate: T | null | undefined, message?: string) {
|
||||
if (!predicate) {
|
||||
message = message || `expect ${predicate} to be true`
|
||||
throw new AssertionError(message)
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
const reported:{[key: string]: boolean} = {}
|
||||
|
||||
export function deprecate (msg: string, issue: number) {
|
||||
if (reported[msg]) return
|
||||
console.warn(msg + ` See: https://github.com/harttle/liquidjs/issues/${issue}`)
|
||||
reported[msg] = true
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
/*
|
||||
* Call functions in serial until someone rejected.
|
||||
* @param {Array} iterable the array to iterate with.
|
||||
* @param {Array} iteratee returns a new promise.
|
||||
* The iteratee is invoked with three arguments: (value, index, iterable).
|
||||
*/
|
||||
export function mapSeries<T1, T2> (
|
||||
iterable: T1[],
|
||||
iteratee: (item: T1, idx: number, iterable: T1[]) => Promise<T2> | T2
|
||||
): Promise<T2[]> {
|
||||
let ret = Promise.resolve(0)
|
||||
const result: T2[] = []
|
||||
iterable.forEach(function (item, idx) {
|
||||
ret = ret
|
||||
.then(() => iteratee(item, idx, iterable))
|
||||
.then(x => result.push(x))
|
||||
})
|
||||
return ret.then(() => result)
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { deprecate } from './deprecate'
|
||||
const toStr = Object.prototype.toString
|
||||
|
||||
/*
|
||||
@@ -29,26 +28,14 @@ export function promisify (fn: any) {
|
||||
export function stringify (value: any): string {
|
||||
if (isNil(value)) return ''
|
||||
value = toLiquid(value)
|
||||
if (isFunction(value.to_s)) {
|
||||
deprecate('to_s is deprecated, use toString instead.', 109)
|
||||
return value.to_s()
|
||||
}
|
||||
return String(value)
|
||||
}
|
||||
|
||||
export function toLiquid (value: any): any {
|
||||
if (isFunction(value.to_liquid)) {
|
||||
deprecate('to_liquid is deprecated, use toLiquid instead.', 109)
|
||||
return toLiquid(value.to_liquid())
|
||||
}
|
||||
if (isFunction(value.toLiquid)) return toLiquid(value.toLiquid())
|
||||
return value
|
||||
}
|
||||
|
||||
export function create<T1 extends object, T2 extends T1 = T1> (proto: T1): T2 {
|
||||
return Object.create(proto)
|
||||
}
|
||||
|
||||
export function isNil (value: any): boolean {
|
||||
return value === null || value === undefined
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ describe('.evalValue()', function () {
|
||||
var engine: Liquid
|
||||
beforeEach(() => { engine = new Liquid() })
|
||||
|
||||
it('should throw when scope undefined', function () {
|
||||
expect(() => engine.evalValue('{{"foo"}}', null as any)).to.throw(/scope undefined/)
|
||||
it('should throw when scope undefined', async function () {
|
||||
return expect(engine.evalValue('{{"foo"}}', null as any)).to.be.rejectedWith(/scope undefined/)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -33,9 +33,9 @@ describe('filters/array', function () {
|
||||
' | split: ", " %}{{ my_array | size }}',
|
||||
'4')
|
||||
})
|
||||
it('should also be used with dot notation - string',
|
||||
it('should be respected with <string>.size notation',
|
||||
() => test('{% assign my_string = "Ground control to Major Tom." %}{{ my_string.size }}', '28'))
|
||||
it('should also be used with dot notation - array',
|
||||
it('should be respected with <array>.size notation',
|
||||
() => test('{% assign my_array = "apples, oranges, peaches, plums" | split: ", " %}{{ my_array.size }}', '4'))
|
||||
})
|
||||
describe('slice', function () {
|
||||
@@ -43,6 +43,7 @@ describe('filters/array', function () {
|
||||
it('should slice third char by 2', () => test('{{ "Liquid" | slice: 2 }}', 'q'))
|
||||
it('should slice substr by 2,5', () => test('{{ "Liquid" | slice: 2, 5 }}', 'quid'))
|
||||
it('should slice substr by -3,2', () => test('{{ "Liquid" | slice: -3, 2 }}', 'ui'))
|
||||
it('should slice substr by -2,2', () => test('{{ "abc" | slice: -2, 2 }}', 'bc'))
|
||||
it('should support array', () => test('{{ "1,2,3,4" | split: "," | slice: 1,2 | join }}', '2 3'))
|
||||
})
|
||||
it('should support sort', function () {
|
||||
@@ -63,4 +64,28 @@ describe('filters/array', function () {
|
||||
return test('{{"" | uniq | join: ","}}', '')
|
||||
})
|
||||
})
|
||||
describe('where', function () {
|
||||
it('should support filter by property value', function () {
|
||||
return test(`{% assign kitchen_products = products | where: "type", "kitchen" %}
|
||||
Kitchen products:
|
||||
{% for product in kitchen_products -%}
|
||||
- {{ product.title }}
|
||||
{% endfor %}`, `
|
||||
Kitchen products:
|
||||
- Spatula
|
||||
- Garlic press
|
||||
`)
|
||||
})
|
||||
it('should support filter truthy property', function () {
|
||||
return test(`{% assign available_products = products | where: "available" %}
|
||||
Available products:
|
||||
{% for product in available_products -%}
|
||||
- {{ product.title }}
|
||||
{% endfor %}`, `
|
||||
Available products:
|
||||
- Coffee mug
|
||||
- Boring sneakers
|
||||
`)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,7 +2,12 @@ import { test } from '../../../stub/render'
|
||||
|
||||
describe('filters/object', function () {
|
||||
describe('default', function () {
|
||||
it('should use default when falsy', () => test('{{false |default: "a"}}', 'a'))
|
||||
it('should not use default when truthy', () => test('{{true |default: "a"}}', 'true'))
|
||||
it('false should use default', () => test('{{false | default: "a"}}', 'a'))
|
||||
it('empty string should use default', () => test('{{"" | default: "a"}}', 'a'))
|
||||
it('non-empty string should not use default', () => test('{{" " | default: "a"}}', ' '))
|
||||
it('nil should use default', () => test('{{nil | default: "a"}}', 'a'))
|
||||
it('undefined should use default', () => test('{{not_defined | default: "a"}}', 'a'))
|
||||
it('true should not use default', () => test('{{true | default: "a"}}', 'true'))
|
||||
it('0 should not use default', () => test('{{0 | default: "a"}}', '0'))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -21,13 +21,6 @@ describe('tags/assign', function () {
|
||||
const html = await liquid.parseAndRender(src)
|
||||
return expect(html).to.equal('10086')
|
||||
})
|
||||
it('should shading rather than overwriting', async function () {
|
||||
const ctx = { foo: 'foo' }
|
||||
const src = '{% assign foo="FOO" %}{{foo}}'
|
||||
const html = await liquid.parseAndRender(src, ctx)
|
||||
expect(html).to.equal('FOO')
|
||||
expect(ctx.foo).to.equal('foo')
|
||||
})
|
||||
it('should assign as array', async function () {
|
||||
const src = '{% assign foo=(1..3) %}{{foo}}'
|
||||
const html = await liquid.parseAndRender(src)
|
||||
@@ -76,4 +69,16 @@ describe('tags/assign', function () {
|
||||
const html = await liquid.parseAndRender(src)
|
||||
return expect(html).to.equal('-6')
|
||||
})
|
||||
describe('scope', function () {
|
||||
it('should read from parent scope', async function () {
|
||||
const src = '{%for a in (1..2)%}{{num}}{%endfor%}'
|
||||
const html = await liquid.parseAndRender(src, { num: 1 })
|
||||
return expect(html).to.equal('11')
|
||||
})
|
||||
it('should write to the belonging scope', async function () {
|
||||
const src = '{%for a in (1..2)%}{%assign num = a%}{{a}}{%endfor%} {{num}}'
|
||||
const html = await liquid.parseAndRender(src, { num: 1 })
|
||||
return expect(html).to.equal('12 2')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import Liquid from '../../../../src/liquid'
|
||||
import { expect, use } from 'chai'
|
||||
import * as chaiAsPromised from 'chai-as-promised'
|
||||
import { Context } from '../../../../src/scope/scope'
|
||||
import { Scope } from '../../../../src/context/scope'
|
||||
|
||||
use(chaiAsPromised)
|
||||
|
||||
describe('tags/for', function () {
|
||||
let liquid: Liquid, ctx: Context
|
||||
let liquid: Liquid, ctx: Scope
|
||||
before(function () {
|
||||
liquid = new Liquid()
|
||||
liquid.registerTag('throwingTag', {
|
||||
@@ -34,20 +34,11 @@ describe('tags/for', function () {
|
||||
const html = await liquid.parseAndRender(src, ctx)
|
||||
return expect(html).to.equal('foo,bar-coo,haa-')
|
||||
})
|
||||
|
||||
describe('scope', function () {
|
||||
it('should read super scope', async function () {
|
||||
const src = '{%for a in (1..2)%}{{num}}{%endfor%}'
|
||||
const html = await liquid.parseAndRender(src, { num: 1 })
|
||||
return expect(html).to.equal('11')
|
||||
})
|
||||
it('should write super scope', async function () {
|
||||
const src = '{%for a in (1..2)%}{{num}}{%assign num = 2%}{%endfor%}'
|
||||
const html = await liquid.parseAndRender(src, { num: 1 })
|
||||
return expect(html).to.equal('12')
|
||||
})
|
||||
it('should output forloop', async function () {
|
||||
const src = '{%for i in (1..1)%}{{forloop}}{%endfor%}'
|
||||
const html = await liquid.parseAndRender(src, ctx)
|
||||
return expect(html).to.equal('{"i":0,"length":1}')
|
||||
})
|
||||
|
||||
describe('illegal', function () {
|
||||
it('should reject when for not closed', function () {
|
||||
const src = '{%for c in alpha%}{{c}}'
|
||||
|
||||
@@ -69,10 +69,42 @@ describe('tags/tablerow', function () {
|
||||
return expect(html).to.equal(dst)
|
||||
})
|
||||
|
||||
it('should support tablerow with offset', async function () {
|
||||
const src = '{% tablerow i in (1..5) cols:2 offset:3 %}{{ i }}{% endtablerow %}'
|
||||
const dst = '<tr class="row1"><td class="col1">4</td><td class="col2">5</td></tr>'
|
||||
it('should support index0, index, rindex0, rindex', async function () {
|
||||
const src = '{% tablerow i in (1..3)%}{{tablerowloop.index0}}{{tablerowloop.index}}{{tablerowloop.rindex0}}{{tablerowloop.rindex}}{% endtablerow %}'
|
||||
const dst = '<tr class="row1"><td class="col1">0123</td><td class="col2">1212</td><td class="col3">2301</td></tr>'
|
||||
const html = await liquid.parseAndRender(src)
|
||||
return expect(html).to.equal(dst)
|
||||
})
|
||||
it('should support first, last, length', async function () {
|
||||
const src = '{% tablerow i in (1..3)%}{{tablerowloop.first}} {{tablerowloop.last}} {{tablerowloop.length}}{% endtablerow %}'
|
||||
const dst = '<tr class="row1"><td class="col1">true false 3</td><td class="col2">false false 3</td><td class="col3">false true 3</td></tr>'
|
||||
const html = await liquid.parseAndRender(src)
|
||||
return expect(html).to.equal(dst)
|
||||
})
|
||||
it('should support col, row, col0, col_first, col_last', async function () {
|
||||
const src = '{% tablerow i in (1..3)%}{{tablerowloop.col}} {{tablerowloop.col0}} {{tablerowloop.col_first}} {{tablerowloop.col_last}}{% endtablerow %}'
|
||||
const dst = '<tr class="row1"><td class="col1">1 0 true false</td><td class="col2">2 1 false false</td><td class="col3">3 2 false true</td></tr>'
|
||||
const html = await liquid.parseAndRender(src)
|
||||
return expect(html).to.equal(dst)
|
||||
})
|
||||
describe('offset', function () {
|
||||
it('should support tablerow with offset', async function () {
|
||||
const src = '{% tablerow i in (1..5) cols:2 offset:3 %}{{ i }}{% endtablerow %}'
|
||||
const dst = '<tr class="row1"><td class="col1">4</td><td class="col2">5</td></tr>'
|
||||
const html = await liquid.parseAndRender(src)
|
||||
return expect(html).to.equal(dst)
|
||||
})
|
||||
it('index should also start at 1', async function () {
|
||||
const src = '{% tablerow i in (1..4) cols:2 offset:2 %}{{tablerowloop.index}}{% endtablerow %}'
|
||||
const dst = '<tr class="row1"><td class="col1">1</td><td class="col2">2</td></tr>'
|
||||
const html = await liquid.parseAndRender(src)
|
||||
return expect(html).to.equal(dst)
|
||||
})
|
||||
it('col should also start at 1', async function () {
|
||||
const src = '{% tablerow i in (1..4) cols:2 offset:3 %}{{tablerowloop.col}}{% endtablerow %}'
|
||||
const dst = '<tr class="row1"><td class="col1">1</td></tr>'
|
||||
const html = await liquid.parseAndRender(src)
|
||||
return expect(html).to.equal(dst)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,7 +8,7 @@ describe('drop/drop', function () {
|
||||
class CustomDrop extends Liquid.Types.Drop {
|
||||
name: string = 'NAME'
|
||||
getName () {
|
||||
return 'GETNAME'
|
||||
return 'GET NAME'
|
||||
}
|
||||
}
|
||||
class CustomDropWithMethodMissing extends CustomDrop {
|
||||
@@ -16,9 +16,18 @@ describe('drop/drop', function () {
|
||||
return key.toUpperCase()
|
||||
}
|
||||
}
|
||||
class PromiseDrop extends Liquid.Types.Drop {
|
||||
name = Promise.resolve('NAME')
|
||||
async getName () {
|
||||
return 'GET NAME'
|
||||
}
|
||||
async liquidMethodMissing (key: string) {
|
||||
return key.toUpperCase()
|
||||
}
|
||||
}
|
||||
it('should call corresponding method', async function () {
|
||||
const html = await liquid.parseAndRender(`{{obj.getName}}`, { obj: new CustomDrop() })
|
||||
expect(html).to.equal('GETNAME')
|
||||
expect(html).to.equal('GET NAME')
|
||||
})
|
||||
it('should read corresponding property', async function () {
|
||||
const html = await liquid.parseAndRender(`{{obj.name}}`, { obj: new CustomDrop() })
|
||||
@@ -32,4 +41,16 @@ describe('drop/drop', function () {
|
||||
const html = await liquid.parseAndRender(`{{obj.foo}}`, { obj: new CustomDropWithMethodMissing() })
|
||||
expect(html).to.equal('FOO')
|
||||
})
|
||||
it('should call corresponding promise method', async function () {
|
||||
const html = await liquid.parseAndRender(`{{obj.getName}}`, { obj: new PromiseDrop() })
|
||||
expect(html).to.equal('GET NAME')
|
||||
})
|
||||
it('should read corresponding promise property', async function () {
|
||||
const html = await liquid.parseAndRender(`{{obj.name}}`, { obj: new PromiseDrop() })
|
||||
expect(html).to.equal('NAME')
|
||||
})
|
||||
it('should support promise returned by liquidMethodMissing', async function () {
|
||||
const html = await liquid.parseAndRender(`{{obj.foo}}`, { obj: new PromiseDrop() })
|
||||
expect(html).to.equal('FOO')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { test, liquid } from '../../stub/render'
|
||||
|
||||
describe('liquid#registerFilter()', function () {
|
||||
describe('object arguments', function () {
|
||||
liquid.registerFilter('obj_test', function () {
|
||||
return JSON.stringify(arguments)
|
||||
})
|
||||
it('should support object', () => test(
|
||||
`{{ "a" | obj_test: k1: "v1", k2: foo }}`,
|
||||
'{"0":"a","1":["k1","v1"],"2":["k2","bar"]}'
|
||||
))
|
||||
it('should support mixed object', () => test(
|
||||
`{{ "a" | obj_test: "something", k1: "v1", k2: foo }}`,
|
||||
'{"0":"a","1":"something","2":["k1","v1"],"3":["k2","bar"]}'
|
||||
))
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
import Liquid from '../../../src/liquid'
|
||||
import { expect } from 'chai'
|
||||
|
||||
describe('LiquidOptions#strict_*', function () {
|
||||
describe('LiquidOptions#strict*', function () {
|
||||
let engine: Liquid
|
||||
const ctx = {}
|
||||
beforeEach(function () {
|
||||
|
||||
@@ -5,35 +5,35 @@ describe('LiquidOptions#trimming', function () {
|
||||
const ctx = { name: 'harttle' }
|
||||
|
||||
describe('tag trimming', function () {
|
||||
it('should respect trim_tag_left', async function () {
|
||||
const engine = new Liquid({ trim_tag_left: true } as any)
|
||||
it('should respect trimTagLeft', async function () {
|
||||
const engine = new Liquid({ trimTagLeft: true })
|
||||
const html = await engine.parseAndRender(' \n \t{%if true%}foo{%endif%} ')
|
||||
return expect(html).to.equal('foo ')
|
||||
})
|
||||
it('should respect trim_tag_right', async function () {
|
||||
const engine = new Liquid({ trim_tag_right: true } as any)
|
||||
it('should respect trimTagRight', async function () {
|
||||
const engine = new Liquid({ trimTagRight: true } as any)
|
||||
const html = await engine.parseAndRender('\t{%if true%}foo{%endif%} \n')
|
||||
return expect(html).to.equal('\tfoo')
|
||||
})
|
||||
it('should not trim value', async function () {
|
||||
const engine = new Liquid({ trim_tag_left: true, trim_tag_right: true } as any)
|
||||
const engine = new Liquid({ trimTagLeft: true, trimTagRight: true } as any)
|
||||
const html = await engine.parseAndRender('{%if true%}a {{name}} b{%endif%}', ctx)
|
||||
return expect(html).to.equal('a harttle b')
|
||||
})
|
||||
})
|
||||
describe('value trimming', function () {
|
||||
it('should respect trim_output_left', async function () {
|
||||
const engine = new Liquid({ trim_output_left: true } as any)
|
||||
it('should respect trimOutputLeft', async function () {
|
||||
const engine = new Liquid({ trimOutputLeft: true } as any)
|
||||
const html = await engine.parseAndRender(' \n \t{{name}} ', ctx)
|
||||
return expect(html).to.equal('harttle ')
|
||||
})
|
||||
it('should respect trim_output_right', async function () {
|
||||
const engine = new Liquid({ trim_output_right: true } as any)
|
||||
it('should respect trimOutputRight', async function () {
|
||||
const engine = new Liquid({ trimOutputRight: true } as any)
|
||||
const html = await engine.parseAndRender(' \n \t{{name}} ', ctx)
|
||||
return expect(html).to.equal(' \n \tharttle')
|
||||
})
|
||||
it('should respect not trim tag', async function () {
|
||||
const engine = new Liquid({ trim_output_left: true, trim_output_right: true } as any)
|
||||
const engine = new Liquid({ trimOutputLeft: true, trimOutputRight: true } as any)
|
||||
const html = await engine.parseAndRender('\t{% if true %} aha {%endif%}\t')
|
||||
return expect(html).to.equal('\t aha \t')
|
||||
})
|
||||
|
||||
+3
-2
@@ -1,5 +1,6 @@
|
||||
import { isString, forOwn } from '../../src/util/underscore'
|
||||
import fs from '../../src/fs/node'
|
||||
import { resolve } from 'path'
|
||||
|
||||
type fileDescriptor = { mode: string, content: string }
|
||||
|
||||
@@ -9,8 +10,8 @@ const exists = fs.exists
|
||||
|
||||
export function mock (options: { [path: string]: (string | fileDescriptor) }) {
|
||||
forOwn(options, (val, key) => {
|
||||
files[key] = isString(val)
|
||||
? { mode: '33188', content: val as string }
|
||||
files[resolve(key)] = isString(val)
|
||||
? { mode: '33188', content: val }
|
||||
: val as fileDescriptor
|
||||
})
|
||||
fs.readFile = async function (path) {
|
||||
|
||||
+10
-1
@@ -9,7 +9,16 @@ export const ctx = {
|
||||
arr: [-2, 'a'],
|
||||
obj: { foo: 'bar' },
|
||||
func: function () {},
|
||||
posts: [{ category: 'foo' }, { category: 'bar' }]
|
||||
posts: [{ category: 'foo' }, { category: 'bar' }],
|
||||
products: [
|
||||
{ title: 'Vacuum', type: 'living room' },
|
||||
{ title: 'Spatula', type: 'kitchen' },
|
||||
{ title: 'Television', type: 'living room' },
|
||||
{ title: 'Garlic press', type: 'kitchen' },
|
||||
{ title: 'Coffee mug', available: true },
|
||||
{ title: 'Limited edition sneakers', available: false },
|
||||
{ title: 'Boring sneakers', available: true }
|
||||
]
|
||||
}
|
||||
|
||||
export async function test (src: string, dst: string) {
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
import * as chai from 'chai'
|
||||
import Context from '../../../src/context/context'
|
||||
import { Scope } from '../../../src/context/scope'
|
||||
|
||||
const expect = chai.expect
|
||||
|
||||
describe('scope', function () {
|
||||
let ctx: any, scope: Scope
|
||||
beforeEach(function () {
|
||||
scope = {
|
||||
foo: 'zoo',
|
||||
one: 1,
|
||||
zoo: { size: 4 },
|
||||
bar: {
|
||||
zoo: 'coo',
|
||||
'Mr.Smith': 'John',
|
||||
arr: ['a', 'b']
|
||||
}
|
||||
}
|
||||
ctx = new Context(scope)
|
||||
})
|
||||
|
||||
describe('#propertyAccessSeq()', function () {
|
||||
it('should handle dot syntax', async function () {
|
||||
expect(await ctx.parseProp('foo.bar'))
|
||||
.to.deep.equal(['foo', 'bar'])
|
||||
})
|
||||
it('should handle [<String>] syntax', async function () {
|
||||
expect(await ctx.parseProp('foo["bar"]'))
|
||||
.to.deep.equal(['foo', 'bar'])
|
||||
})
|
||||
it('should handle [<Identifier>] syntax', async function () {
|
||||
expect(await ctx.parseProp('foo[foo]'))
|
||||
.to.deep.equal(['foo', 'zoo'])
|
||||
})
|
||||
it('should handle nested access 1', async function () {
|
||||
expect(await ctx.parseProp('foo[bar.zoo]'))
|
||||
.to.deep.equal(['foo', 'coo'])
|
||||
})
|
||||
it('should handle nested access 2', async function () {
|
||||
expect(await ctx.parseProp('foo[bar["zoo"]]'))
|
||||
.to.deep.equal(['foo', 'coo'])
|
||||
})
|
||||
it('should handle nested access 3', async function () {
|
||||
expect(await ctx.parseProp('bar["foo"].zoo'))
|
||||
.to.deep.equal(['bar', 'foo', 'zoo'])
|
||||
})
|
||||
it('should handle nested access 4', async function () {
|
||||
expect(await ctx.parseProp('foo[0].bar'))
|
||||
.to.deep.equal(['foo', '0', 'bar'])
|
||||
})
|
||||
it('should handle nested access 5', async function () {
|
||||
expect(await ctx.parseProp('foo[one].bar'))
|
||||
.to.deep.equal(['foo', '1', 'bar'])
|
||||
})
|
||||
it('should handle nested access 6', async function () {
|
||||
expect(await ctx.parseProp('foo[two].bar'))
|
||||
.to.deep.equal(['foo', 'undefined', 'bar'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('#get()', function () {
|
||||
it('should get direct property', async function () {
|
||||
expect(await await ctx.get('foo')).equal('zoo')
|
||||
})
|
||||
|
||||
it('undefined property should yield undefined', async function () {
|
||||
expect(ctx.get('notdefined')).to.be.rejected
|
||||
expect(await ctx.get('notdefined')).to.equal(undefined)
|
||||
expect(await ctx.get(false as any)).to.equal(undefined)
|
||||
})
|
||||
|
||||
it('should throw for invalid path', async function () {
|
||||
expect(ctx.get('')).to.be.rejectedWith('invalid path:""')
|
||||
})
|
||||
|
||||
it('should throw when [] unbalanced', async function () {
|
||||
expect(ctx.get('foo[bar')).to.be.rejectedWith(/unbalanced \[\]/)
|
||||
})
|
||||
|
||||
it('should throw when "" unbalanced', async function () {
|
||||
expect(ctx.get('foo["bar]')).to.be.rejectedWith(/unbalanced "/)
|
||||
})
|
||||
|
||||
it("should throw when '' unbalanced", async function () {
|
||||
expect(ctx.get("foo['bar]")).to.be.rejectedWith(/unbalanced '/)
|
||||
})
|
||||
it('should respect to toLiquid', async function () {
|
||||
const scope = new Context({ foo: {
|
||||
toLiquid: () => ({ bar: 'BAR' }),
|
||||
bar: 'bar'
|
||||
} })
|
||||
expect(await scope.get('foo.bar')).to.equal('BAR')
|
||||
})
|
||||
|
||||
it('should access child property via dot syntax', async function () {
|
||||
expect(await ctx.get('bar.zoo')).to.equal('coo')
|
||||
expect(await ctx.get('bar.arr')).to.deep.equal(['a', 'b'])
|
||||
})
|
||||
|
||||
it('should access child property via [<String>] syntax', async function () {
|
||||
expect(await ctx.get('bar["zoo"]')).to.equal('coo')
|
||||
})
|
||||
|
||||
it('should access child property via [<Number>] syntax', async function () {
|
||||
expect(await ctx.get('bar.arr[0]')).to.equal('a')
|
||||
})
|
||||
|
||||
it('should access child property via [<Identifier>] syntax', async function () {
|
||||
expect(await ctx.get('bar[foo]')).to.equal('coo')
|
||||
})
|
||||
|
||||
it('should return undefined when not exist', async function () {
|
||||
expect(await ctx.get('foo.foo.foo')).to.be.undefined
|
||||
})
|
||||
it('should return string length as size', async function () {
|
||||
expect(await ctx.get('foo.size')).to.equal(3)
|
||||
})
|
||||
it('should return array length as size', async function () {
|
||||
expect(await ctx.get('bar.arr.size')).to.equal(2)
|
||||
})
|
||||
it('should return size property if exists', async function () {
|
||||
expect(await ctx.get('zoo.size')).to.equal(4)
|
||||
})
|
||||
it('should return undefined if do not have size and length', async function () {
|
||||
expect(await ctx.get('one.size')).to.equal(undefined)
|
||||
})
|
||||
})
|
||||
describe('strictVariables', async function () {
|
||||
let ctx: Context
|
||||
beforeEach(function () {
|
||||
ctx = new Context(ctx, {
|
||||
strictVariables: true
|
||||
} as any)
|
||||
})
|
||||
it('should throw when variable not defined', function () {
|
||||
return expect(ctx.get('notdefined')).to.be.rejectedWith(/undefined variable: notdefined/)
|
||||
})
|
||||
it('should throw when deep variable not exist', async function () {
|
||||
ctx.push({ foo: 'FOO' })
|
||||
return expect(ctx.get('foo.bar.not.defined')).to.be.rejectedWith(/undefined variable: bar/)
|
||||
})
|
||||
it('should throw when itself not defined', async function () {
|
||||
ctx.push({ foo: 'FOO' })
|
||||
return expect(ctx.get('foo.BAR')).to.be.rejectedWith(/undefined variable: BAR/)
|
||||
})
|
||||
it('should find variable in parent scope', async function () {
|
||||
ctx.push({ 'foo': 'foo' })
|
||||
ctx.push({
|
||||
'bar': 'bar'
|
||||
})
|
||||
expect(await ctx.get('foo')).to.equal('foo')
|
||||
})
|
||||
})
|
||||
|
||||
describe('.getAll()', function () {
|
||||
it('should get all properties when arguments empty', async function () {
|
||||
expect(await ctx.getAll()).deep.equal(scope)
|
||||
})
|
||||
})
|
||||
|
||||
describe('.push()', function () {
|
||||
it('should push scope', async function () {
|
||||
ctx.push({ 'bar': 'bar' })
|
||||
ctx.push({
|
||||
foo: 'foo'
|
||||
})
|
||||
expect(await ctx.get('foo')).to.equal('foo')
|
||||
expect(await ctx.get('bar')).to.equal('bar')
|
||||
})
|
||||
it('should hide deep properties by push', async function () {
|
||||
ctx.push({ bar: { bar: 'bar' } })
|
||||
ctx.push({ bar: { foo: 'foo' } })
|
||||
expect(await ctx.get('bar.foo')).to.equal('foo')
|
||||
expect(await ctx.get('bar.bar')).to.equal(undefined)
|
||||
})
|
||||
})
|
||||
describe('.pop()', function () {
|
||||
it('should pop scope', async function () {
|
||||
ctx.push({
|
||||
foo: 'foo'
|
||||
})
|
||||
ctx.pop()
|
||||
expect(await ctx.get('foo')).to.equal('zoo')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,8 +1,8 @@
|
||||
import { expect } from 'chai'
|
||||
import Scope from '../../../src/scope/scope'
|
||||
import Context from '../../../src/context/context'
|
||||
import Token from '../../../src/parser/token'
|
||||
import Tag from '../../../src/template/tag/tag'
|
||||
import Filter from '../../../src/template/filter/filter'
|
||||
import { Filter } from '../../../src/template/filter/filter'
|
||||
import Render from '../../../src/render/render'
|
||||
import HTML from '../../../src/template/html'
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('render', function () {
|
||||
})
|
||||
|
||||
it('should render html', async function () {
|
||||
const scope = new Scope()
|
||||
const scope = new Context()
|
||||
const token = { type: 'html', value: '<p>' } as Token
|
||||
const html = await render.renderTemplates([new HTML(token)], scope)
|
||||
return expect(html).to.equal('<p>')
|
||||
|
||||
+49
-51
@@ -1,12 +1,12 @@
|
||||
import Scope from '../../../src/scope/scope'
|
||||
import Context from '../../../src/context/context'
|
||||
import { expect } from 'chai'
|
||||
import { evalExp, evalValue, isTruthy } from '../../../src/render/syntax'
|
||||
|
||||
describe('render/syntax', function () {
|
||||
let scope: Scope
|
||||
let ctx: Context
|
||||
|
||||
beforeEach(function () {
|
||||
scope = new Scope({
|
||||
ctx = new Context({
|
||||
one: 1,
|
||||
two: 2,
|
||||
empty: '',
|
||||
@@ -18,35 +18,35 @@ describe('render/syntax', function () {
|
||||
})
|
||||
|
||||
describe('.evalValue()', function () {
|
||||
it('should eval boolean literal', function () {
|
||||
expect(evalValue('true', scope)).to.equal(true)
|
||||
expect(evalValue('TrUE', scope)).to.equal(undefined)
|
||||
expect(evalValue('false', scope)).to.equal(false)
|
||||
it('should eval boolean literal', async function () {
|
||||
expect(await evalValue('true', ctx)).to.equal(true)
|
||||
expect(await evalValue('TrUE', ctx)).to.equal(undefined)
|
||||
expect(await evalValue('false', ctx)).to.equal(false)
|
||||
})
|
||||
it('should eval number literal', function () {
|
||||
expect(evalValue('2.3', scope)).to.equal(2.3)
|
||||
expect(evalValue('.32', scope)).to.equal(0.32)
|
||||
expect(evalValue('-23.', scope)).to.equal(-23)
|
||||
expect(evalValue('23', scope)).to.equal(23)
|
||||
it('should eval number literal', async function () {
|
||||
expect(await evalValue('2.3', ctx)).to.equal(2.3)
|
||||
expect(await evalValue('.32', ctx)).to.equal(0.32)
|
||||
expect(await evalValue('-23.', ctx)).to.equal(-23)
|
||||
expect(await evalValue('23', ctx)).to.equal(23)
|
||||
})
|
||||
it('should eval string literal', function () {
|
||||
expect(evalValue('"ab\'c"', scope)).to.equal("ab'c")
|
||||
expect(evalValue("'ab\"c'", scope)).to.equal('ab"c')
|
||||
it('should eval string literal', async function () {
|
||||
expect(await evalValue('"ab\'c"', ctx)).to.equal("ab'c")
|
||||
expect(await evalValue("'ab\"c'", ctx)).to.equal('ab"c')
|
||||
})
|
||||
it('should eval nil literal', function () {
|
||||
expect(evalValue('nil', scope)).to.be.null
|
||||
it('should eval nil literal', async function () {
|
||||
expect(await evalValue('nil', ctx)).to.be.null
|
||||
})
|
||||
it('should eval null literal', function () {
|
||||
expect(evalValue('null', scope)).to.be.null
|
||||
it('should eval null literal', async function () {
|
||||
expect(await evalValue('null', ctx)).to.be.null
|
||||
})
|
||||
it('should eval scope variables', function () {
|
||||
expect(evalValue('one', scope)).to.equal(1)
|
||||
expect(evalValue('has_value?', scope)).to.equal(true)
|
||||
expect(evalValue('x', scope)).to.equal('XXX')
|
||||
it('should eval scope variables', async function () {
|
||||
expect(await evalValue('one', ctx)).to.equal(1)
|
||||
expect(await evalValue('has_value?', ctx)).to.equal(true)
|
||||
expect(await evalValue('x', ctx)).to.equal('XXX')
|
||||
})
|
||||
})
|
||||
|
||||
describe('.isTruthy()', function () {
|
||||
describe('.isTruthy()', async function () {
|
||||
// Spec: https://shopify.github.io/liquid/basics/truthy-and-falsy/
|
||||
expect(isTruthy(true)).to.be.true
|
||||
expect(isTruthy(false)).to.be.false
|
||||
@@ -61,44 +61,42 @@ describe('render/syntax', function () {
|
||||
})
|
||||
|
||||
describe('.evalExp()', function () {
|
||||
it('should throw when scope undefined', function () {
|
||||
expect(function () {
|
||||
(evalExp as any)('')
|
||||
}).to.throw(/scope undefined/)
|
||||
it('should throw when scope undefined', async function () {
|
||||
return expect((evalExp as any)('')).to.be.rejectedWith(/scope undefined/)
|
||||
})
|
||||
|
||||
it('should eval simple expression', function () {
|
||||
expect(evalExp('1<2', scope)).to.equal(true)
|
||||
expect(evalExp('2<=2', scope)).to.equal(true)
|
||||
expect(evalExp('one<=two', scope)).to.equal(true)
|
||||
expect(evalExp('x contains "x"', scope)).to.equal(false)
|
||||
expect(evalExp('x contains "X"', scope)).to.equal(true)
|
||||
expect(evalExp('1 contains "x"', scope)).to.equal(false)
|
||||
expect(evalExp('y contains "x"', scope)).to.equal(false)
|
||||
expect(evalExp('z contains "x"', scope)).to.equal(false)
|
||||
expect(evalExp('(1..5) contains 3', scope)).to.equal(true)
|
||||
expect(evalExp('(1..5) contains 6', scope)).to.equal(false)
|
||||
expect(evalExp('"<=" == "<="', scope)).to.equal(true)
|
||||
it('should eval simple expression', async function () {
|
||||
expect(await evalExp('1<2', ctx)).to.equal(true)
|
||||
expect(await evalExp('2<=2', ctx)).to.equal(true)
|
||||
expect(await evalExp('one<=two', ctx)).to.equal(true)
|
||||
expect(await evalExp('x contains "x"', ctx)).to.equal(false)
|
||||
expect(await evalExp('x contains "X"', ctx)).to.equal(true)
|
||||
expect(await evalExp('1 contains "x"', ctx)).to.equal(false)
|
||||
expect(await evalExp('y contains "x"', ctx)).to.equal(false)
|
||||
expect(await evalExp('z contains "x"', ctx)).to.equal(false)
|
||||
expect(await evalExp('(1..5) contains 3', ctx)).to.equal(true)
|
||||
expect(await evalExp('(1..5) contains 6', ctx)).to.equal(false)
|
||||
expect(await evalExp('"<=" == "<="', ctx)).to.equal(true)
|
||||
})
|
||||
|
||||
describe('complex expression', function () {
|
||||
it('should support value or value', function () {
|
||||
expect(evalExp('false or true', scope)).to.equal(true)
|
||||
it('should support value or value', async function () {
|
||||
expect(await evalExp('false or true', ctx)).to.equal(true)
|
||||
})
|
||||
it('should support < and contains', function () {
|
||||
expect(evalExp('1<2 and x contains "x"', scope)).to.equal(false)
|
||||
it('should support < and contains', async function () {
|
||||
expect(await evalExp('1<2 and x contains "x"', ctx)).to.equal(false)
|
||||
})
|
||||
it('should support < or contains', function () {
|
||||
expect(evalExp('1<2 or x contains "x"', scope)).to.equal(true)
|
||||
it('should support < or contains', async function () {
|
||||
expect(await evalExp('1<2 or x contains "x"', ctx)).to.equal(true)
|
||||
})
|
||||
it('should support value and !=', function () {
|
||||
expect(evalExp('empty and empty != ""', scope)).to.equal(false)
|
||||
it('should support value and !=', async function () {
|
||||
expect(await evalExp('empty and empty != ""', ctx)).to.equal(false)
|
||||
})
|
||||
})
|
||||
|
||||
it('should eval range expression', function () {
|
||||
expect(evalExp('(2..4)', scope)).to.deep.equal([2, 3, 4])
|
||||
expect(evalExp('(two..4)', scope)).to.deep.equal([2, 3, 4])
|
||||
it('should eval range expression', async function () {
|
||||
expect(await evalExp('(2..4)', ctx)).to.deep.equal([2, 3, 4])
|
||||
expect(await evalExp('(two..4)', ctx)).to.deep.equal([2, 3, 4])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,272 +0,0 @@
|
||||
import * as chai from 'chai'
|
||||
import Scope, { Context } from '../../../src/scope/scope'
|
||||
|
||||
const expect = chai.expect
|
||||
|
||||
describe('scope', function () {
|
||||
let scope: Scope, ctx: Context
|
||||
beforeEach(function () {
|
||||
ctx = {
|
||||
foo: 'zoo',
|
||||
one: 1,
|
||||
zoo: { size: 4 },
|
||||
bar: {
|
||||
zoo: 'coo',
|
||||
'Mr.Smith': 'John',
|
||||
arr: ['a', 'b']
|
||||
}
|
||||
}
|
||||
scope = new Scope(ctx)
|
||||
})
|
||||
|
||||
describe('#propertyAccessSeq()', function () {
|
||||
it('should handle dot syntax', function () {
|
||||
expect(scope.propertyAccessSeq('foo.bar'))
|
||||
.to.deep.equal(['foo', 'bar'])
|
||||
})
|
||||
it('should handle [<String>] syntax', function () {
|
||||
expect(scope.propertyAccessSeq('foo["bar"]'))
|
||||
.to.deep.equal(['foo', 'bar'])
|
||||
})
|
||||
it('should handle [<Identifier>] syntax', function () {
|
||||
expect(scope.propertyAccessSeq('foo[foo]'))
|
||||
.to.deep.equal(['foo', 'zoo'])
|
||||
})
|
||||
it('should handle nested access 1', function () {
|
||||
expect(scope.propertyAccessSeq('foo[bar.zoo]'))
|
||||
.to.deep.equal(['foo', 'coo'])
|
||||
})
|
||||
it('should handle nested access 2', function () {
|
||||
expect(scope.propertyAccessSeq('foo[bar["zoo"]]'))
|
||||
.to.deep.equal(['foo', 'coo'])
|
||||
})
|
||||
it('should handle nested access 3', function () {
|
||||
expect(scope.propertyAccessSeq('bar["foo"].zoo'))
|
||||
.to.deep.equal(['bar', 'foo', 'zoo'])
|
||||
})
|
||||
it('should handle nested access 4', function () {
|
||||
expect(scope.propertyAccessSeq('foo[0].bar'))
|
||||
.to.deep.equal(['foo', '0', 'bar'])
|
||||
})
|
||||
it('should handle nested access 5', function () {
|
||||
expect(scope.propertyAccessSeq('foo[one].bar'))
|
||||
.to.deep.equal(['foo', '1', 'bar'])
|
||||
})
|
||||
it('should handle nested access 6', function () {
|
||||
expect(scope.propertyAccessSeq('foo[two].bar'))
|
||||
.to.deep.equal(['foo', 'undefined', 'bar'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('#get()', function () {
|
||||
it('should get direct property', function () {
|
||||
expect(scope.get('foo')).equal('zoo')
|
||||
})
|
||||
|
||||
it('undefined property should yield undefined', function () {
|
||||
function fn () {
|
||||
scope.get('notdefined')
|
||||
}
|
||||
expect(fn).to.not.throw()
|
||||
expect(scope.get('notdefined')).to.equal(undefined)
|
||||
expect(scope.get(false as any)).to.equal(undefined)
|
||||
})
|
||||
|
||||
it('should throw for invalid path', function () {
|
||||
function fn () {
|
||||
scope.get('')
|
||||
}
|
||||
expect(fn).to.throw('invalid path:""')
|
||||
})
|
||||
|
||||
it('should throw when [] unbalanced', function () {
|
||||
expect(function () {
|
||||
scope.get('foo[bar')
|
||||
}).to.throw(/unbalanced \[\]/)
|
||||
})
|
||||
|
||||
it('should throw when "" unbalanced', function () {
|
||||
expect(function () {
|
||||
scope.get('foo["bar]')
|
||||
}).to.throw(/unbalanced "/)
|
||||
})
|
||||
|
||||
it("should throw when '' unbalanced", function () {
|
||||
expect(function () {
|
||||
scope.get("foo['bar]")
|
||||
}).to.throw(/unbalanced '/)
|
||||
})
|
||||
|
||||
it('should respect to to_liquid', function () {
|
||||
const scope = new Scope({ foo: {
|
||||
to_liquid: () => ({ bar: 'BAR' }),
|
||||
bar: 'bar'
|
||||
} })
|
||||
expect(scope.get('foo.bar')).to.equal('BAR')
|
||||
})
|
||||
|
||||
it('should respect to toLiquid', function () {
|
||||
const scope = new Scope({ foo: {
|
||||
toLiquid: () => ({ bar: 'BAR' }),
|
||||
bar: 'bar'
|
||||
} })
|
||||
expect(scope.get('foo.bar')).to.equal('BAR')
|
||||
})
|
||||
|
||||
it('should access child property via dot syntax', function () {
|
||||
expect(scope.get('bar.zoo')).to.equal('coo')
|
||||
expect(scope.get('bar.arr')).to.deep.equal(['a', 'b'])
|
||||
})
|
||||
|
||||
it('should access child property via [<String>] syntax', function () {
|
||||
expect(scope.get('bar["zoo"]')).to.equal('coo')
|
||||
})
|
||||
|
||||
it('should access child property via [<Number>] syntax', function () {
|
||||
expect(scope.get('bar.arr[0]')).to.equal('a')
|
||||
})
|
||||
|
||||
it('should access child property via [<Identifier>] syntax', function () {
|
||||
expect(scope.get('bar[foo]')).to.equal('coo')
|
||||
})
|
||||
|
||||
it('should return undefined when not exist', function () {
|
||||
expect(scope.get('foo.foo.foo')).to.be.undefined
|
||||
})
|
||||
it('should return string length as size', function () {
|
||||
expect(scope.get('foo.size')).to.equal(3)
|
||||
})
|
||||
it('should return array length as size', function () {
|
||||
expect(scope.get('bar.arr.size')).to.equal(2)
|
||||
})
|
||||
it('should return size property if exists', function () {
|
||||
expect(scope.get('zoo.size')).to.equal(4)
|
||||
})
|
||||
it('should return undefined if do not have size and length', function () {
|
||||
expect(scope.get('one.size')).to.equal(undefined)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#set', function () {
|
||||
it('should set nested value', function () {
|
||||
scope.set('posts', {
|
||||
'first': {
|
||||
'name': 'A Nice Day'
|
||||
}
|
||||
})
|
||||
scope.set('category', {
|
||||
'diary': ['first']
|
||||
})
|
||||
expect(scope.get('posts[category.diary[0]].name'), 'A Nice Day')
|
||||
})
|
||||
|
||||
it('should create parent if needed', function () {
|
||||
scope.set('a.b.c.d', 'COO')
|
||||
expect(scope.get('a.b.c.d')).to.equal('COO')
|
||||
})
|
||||
it('should keep other properties of parent', function () {
|
||||
scope.push({ obj: { foo: 'FOO' } })
|
||||
scope.set('obj.bar', 'BAR')
|
||||
expect(scope.get('obj.foo')).to.equal('FOO')
|
||||
})
|
||||
it('should abort if property cannot be set', function () {
|
||||
scope.push({ obj: { foo: 'FOO' } })
|
||||
scope.set('obj.foo.bar', 'BAR')
|
||||
expect(scope.get('obj.foo')).to.equal('FOO')
|
||||
})
|
||||
it("should set parents' corresponding value", function () {
|
||||
scope.push({})
|
||||
scope.set('foo', 'bar')
|
||||
scope.pop()
|
||||
expect(scope.get('foo')).to.equal('bar')
|
||||
})
|
||||
})
|
||||
describe('strictVariables', function () {
|
||||
let scope: Scope
|
||||
beforeEach(function () {
|
||||
scope = new Scope(ctx, {
|
||||
strictVariables: true
|
||||
} as any)
|
||||
})
|
||||
it('should throw when variable not defined', function () {
|
||||
function fn () {
|
||||
scope.get('notdefined')
|
||||
}
|
||||
expect(fn).to.throw(/undefined variable: notdefined/)
|
||||
})
|
||||
it('should throw when deep variable not exist', function () {
|
||||
scope.set('foo', 'FOO')
|
||||
function fn () {
|
||||
scope.get('foo.bar.not.defined')
|
||||
}
|
||||
expect(fn).to.throw(/undefined variable: bar/)
|
||||
})
|
||||
it('should throw when itself not defined', function () {
|
||||
scope.set('foo', 'bar')
|
||||
function fn () {
|
||||
scope.get('foo.BAR')
|
||||
}
|
||||
expect(fn).to.throw(/undefined variable: BAR/)
|
||||
})
|
||||
it('should find variable in parent scope', function () {
|
||||
scope.set('foo', 'foo')
|
||||
scope.push({
|
||||
'bar': 'bar'
|
||||
})
|
||||
expect(scope.get('foo')).to.equal('foo')
|
||||
})
|
||||
})
|
||||
|
||||
describe('.getAll()', function () {
|
||||
it('should get all properties when arguments empty', function () {
|
||||
expect(scope.getAll()).deep.equal(ctx)
|
||||
})
|
||||
})
|
||||
|
||||
describe('.push()', function () {
|
||||
it('should push scope', function () {
|
||||
scope.set('bar', 'bar')
|
||||
scope.push({
|
||||
foo: 'foo'
|
||||
})
|
||||
expect(scope.get('foo')).to.equal('foo')
|
||||
expect(scope.get('bar')).to.equal('bar')
|
||||
})
|
||||
it('should hide deep properties by push', function () {
|
||||
scope.set('bar', { bar: 'bar' })
|
||||
scope.push({ bar: { foo: 'foo' } })
|
||||
expect(scope.get('bar.foo')).to.equal('foo')
|
||||
expect(scope.get('bar.bar')).to.equal(undefined)
|
||||
})
|
||||
})
|
||||
describe('.pop()', function () {
|
||||
it('should pop scope', function () {
|
||||
scope.push({
|
||||
foo: 'foo'
|
||||
})
|
||||
scope.pop()
|
||||
expect(scope.get('foo')).to.equal('zoo')
|
||||
})
|
||||
})
|
||||
it('should pop specified scope', function () {
|
||||
const scope1 = {
|
||||
foo: 'foo'
|
||||
}
|
||||
const scope2 = {
|
||||
bar: 'bar'
|
||||
}
|
||||
scope.push(scope1)
|
||||
scope.push(scope2)
|
||||
expect(scope.get('foo')).to.equal('foo')
|
||||
expect(scope.get('bar')).to.equal('bar')
|
||||
scope.pop(scope1)
|
||||
expect(scope.get('foo')).to.equal('zoo')
|
||||
expect(scope.get('bar')).to.equal('bar')
|
||||
})
|
||||
it('should throw when specified scope not found', function () {
|
||||
const scope1 = {
|
||||
foo: 'foo'
|
||||
}
|
||||
expect(() => scope.pop(scope1)).to.throw('scope not found, cannot pop')
|
||||
})
|
||||
})
|
||||
@@ -1,46 +1,52 @@
|
||||
import * as chai from 'chai'
|
||||
import * as sinon from 'sinon'
|
||||
import * as sinonChai from 'sinon-chai'
|
||||
import Filter from '../../../../src/template/filter/filter'
|
||||
import Scope from '../../../../src/scope/scope'
|
||||
import { Filter } from '../../../../src/template/filter/filter'
|
||||
import Context from '../../../../src/context/context'
|
||||
|
||||
chai.use(sinonChai)
|
||||
const expect = chai.expect
|
||||
|
||||
describe('filter', function () {
|
||||
let scope: Scope
|
||||
let ctx: Context
|
||||
beforeEach(function () {
|
||||
Filter.clear()
|
||||
scope = new Scope()
|
||||
ctx = new Context()
|
||||
})
|
||||
it('should create default filter if not registered', function () {
|
||||
it('should create default filter if not registered', async function () {
|
||||
const result = new Filter('foo', [], false)
|
||||
expect(result.name).to.equal('foo')
|
||||
})
|
||||
|
||||
it('should render input if filter not registered', function () {
|
||||
expect(new Filter('undefined', [], false).render('foo', scope)).to.equal('foo')
|
||||
it('should render input if filter not registered', async function () {
|
||||
expect(await new Filter('undefined', [], false).render('foo', ctx)).to.equal('foo')
|
||||
})
|
||||
|
||||
it('should call filter impl with corrct arguments', function () {
|
||||
it('should call filter impl with correct arguments', async function () {
|
||||
const spy = sinon.spy()
|
||||
Filter.register('foo', spy)
|
||||
new Filter('foo', ['33'], false).render('foo', scope)
|
||||
await new Filter('foo', ['33'], false).render('foo', ctx)
|
||||
expect(spy).to.have.been.calledWith('foo', 33)
|
||||
})
|
||||
it('should render a simple filter', function () {
|
||||
it('should call filter impl with correct this arg', async function () {
|
||||
const spy = sinon.spy()
|
||||
Filter.register('foo', spy)
|
||||
await new Filter('foo', ['33'], false).render('foo', ctx)
|
||||
expect(spy).to.have.been.calledOn(sinon.match.has('context', ctx))
|
||||
})
|
||||
it('should render a simple filter', async function () {
|
||||
Filter.register('upcase', x => x.toUpperCase())
|
||||
expect(new Filter('upcase', [], false).render('foo', scope)).to.equal('FOO')
|
||||
expect(await new Filter('upcase', [], false).render('foo', ctx)).to.equal('FOO')
|
||||
})
|
||||
|
||||
it('should render filters with argument', function () {
|
||||
it('should render filters with argument', async function () {
|
||||
Filter.register('add', (a, b) => a + b)
|
||||
expect(new Filter('add', ['2'], false).render(3, scope)).to.equal(5)
|
||||
expect(await new Filter('add', ['2'], false).render(3, ctx)).to.equal(5)
|
||||
})
|
||||
|
||||
it('should render filters with multiple arguments', function () {
|
||||
it('should render filters with multiple arguments', async function () {
|
||||
Filter.register('add', (a, b, c) => a + b + c)
|
||||
expect(new Filter('add', ['2', '"c"'], false).render(3, scope)).to.equal('5c')
|
||||
expect(await new Filter('add', ['2', '"c"'], false).render(3, ctx)).to.equal('5c')
|
||||
})
|
||||
|
||||
it('should not throw when filter name illegal', function () {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as chai from 'chai'
|
||||
import Scope from '../../../src/scope/scope'
|
||||
import Context from '../../../src/context/context'
|
||||
import Output from '../../../src/template/output'
|
||||
import OutputToken from '../../../src/parser/output-token'
|
||||
import Filter from '../../../src/template/filter/filter'
|
||||
import { Filter } from '../../../src/template/filter/filter'
|
||||
|
||||
const expect = chai.expect
|
||||
|
||||
@@ -11,16 +11,8 @@ describe('Output', function () {
|
||||
Filter.clear()
|
||||
})
|
||||
|
||||
it('should respect to .to_liquid() method', async function () {
|
||||
const scope = new Scope({
|
||||
bar: { to_liquid: () => 'custom' }
|
||||
})
|
||||
const output = new Output({ value: 'bar' } as OutputToken, false)
|
||||
const html = await output.render(scope)
|
||||
return expect(html).to.equal('custom')
|
||||
})
|
||||
it('should stringify objects', async function () {
|
||||
const scope = new Scope({
|
||||
const scope = new Context({
|
||||
foo: { obj: { arr: ['a', 2] } }
|
||||
})
|
||||
const output = new Output({ value: 'foo' } as OutputToken, false)
|
||||
@@ -28,25 +20,19 @@ describe('Output', function () {
|
||||
return expect(html).to.equal('[object Object]')
|
||||
})
|
||||
it('should skip function property', async function () {
|
||||
const scope = new Scope({ obj: { foo: 'foo', bar: (x: any) => x } })
|
||||
const scope = new Context({ obj: { foo: 'foo', bar: (x: any) => x } })
|
||||
const output = new Output({ value: 'obj' } as OutputToken, false)
|
||||
const html = await output.render(scope)
|
||||
return expect(html).to.equal('[object Object]')
|
||||
})
|
||||
it('should respect to .toString()', async () => {
|
||||
const scope = new Scope({ obj: { toString: () => 'FOO' } })
|
||||
const output = new Output({ value: 'obj' } as OutputToken, false)
|
||||
const str = await output.render(scope)
|
||||
return expect(str).to.equal('FOO')
|
||||
})
|
||||
it('should respect to .to_s()', async () => {
|
||||
const scope = new Scope({ obj: { to_s: () => 'FOO' } })
|
||||
const scope = new Context({ obj: { toString: () => 'FOO' } })
|
||||
const output = new Output({ value: 'obj' } as OutputToken, false)
|
||||
const str = await output.render(scope)
|
||||
return expect(str).to.equal('FOO')
|
||||
})
|
||||
it('should respect to .toString()', async () => {
|
||||
const scope = new Scope({ obj: { toString: () => 'FOO' } })
|
||||
const scope = new Context({ obj: { toString: () => 'FOO' } })
|
||||
const output = new Output({ value: 'obj' } as OutputToken, false)
|
||||
const str = await output.render(scope)
|
||||
return expect(str).to.equal('FOO')
|
||||
|
||||
+12
-12
@@ -1,6 +1,6 @@
|
||||
import * as chai from 'chai'
|
||||
import Tag from '../../../src/template/tag/tag'
|
||||
import Scope from '../../../src/scope/scope'
|
||||
import Context from '../../../src/context/context'
|
||||
import * as sinon from 'sinon'
|
||||
import * as sinonChai from 'sinon-chai'
|
||||
import Liquid from '../../../src/liquid'
|
||||
@@ -11,9 +11,9 @@ const expect = chai.expect
|
||||
const liquid = new Liquid()
|
||||
|
||||
describe('tag', function () {
|
||||
let scope: Scope
|
||||
let ctx: Context
|
||||
before(function () {
|
||||
scope = new Scope({
|
||||
ctx = new Context({
|
||||
foo: 'bar',
|
||||
arr: [2, 1],
|
||||
bar: {
|
||||
@@ -51,7 +51,7 @@ describe('tag', function () {
|
||||
value: 'foo',
|
||||
name: 'foo'
|
||||
} as TagToken
|
||||
await new Tag(token, [], liquid).render(scope)
|
||||
await new Tag(token, [], liquid).render(ctx)
|
||||
expect(spy).to.have.been.called
|
||||
})
|
||||
|
||||
@@ -70,30 +70,30 @@ describe('tag', function () {
|
||||
} as TagToken
|
||||
})
|
||||
it('should call tag.render with scope', async function () {
|
||||
await new Tag(token, [], liquid).render(scope)
|
||||
expect(spy).to.have.been.calledWithMatch(scope)
|
||||
await new Tag(token, [], liquid).render(ctx)
|
||||
expect(spy).to.have.been.calledWithMatch(ctx)
|
||||
})
|
||||
it('should resolve identifier hash', async function () {
|
||||
await new Tag(token, [], liquid).render(scope)
|
||||
await new Tag(token, [], liquid).render(ctx)
|
||||
expect(spy).to.have.been.calledWithMatch({}, {
|
||||
aa: 'bar'
|
||||
})
|
||||
})
|
||||
it('should accept space between key/value', async function () {
|
||||
await new Tag(token, [], liquid).render(scope)
|
||||
await new Tag(token, [], liquid).render(ctx)
|
||||
expect(spy).to.have.been.calledWithMatch({}, {
|
||||
bb: 2
|
||||
})
|
||||
})
|
||||
it('should resolve number value hash', async function () {
|
||||
await new Tag(token, [], liquid).render(scope)
|
||||
expect(spy).to.have.been.calledWithMatch(scope, {
|
||||
await new Tag(token, [], liquid).render(ctx)
|
||||
expect(spy).to.have.been.calledWithMatch(ctx, {
|
||||
cc: 2.3
|
||||
})
|
||||
})
|
||||
it('should resolve property access hash', async function () {
|
||||
await new Tag(token, [], liquid).render(scope)
|
||||
expect(spy).to.have.been.calledWithMatch(scope, {
|
||||
await new Tag(token, [], liquid).render(ctx)
|
||||
expect(spy).to.have.been.calledWithMatch(ctx, {
|
||||
dd: 'uoo'
|
||||
})
|
||||
})
|
||||
|
||||
+101
-52
@@ -1,8 +1,8 @@
|
||||
import * as chai from 'chai'
|
||||
import * as sinonChai from 'sinon-chai'
|
||||
import * as sinon from 'sinon'
|
||||
import Scope from '../../../src/scope/scope'
|
||||
import Filter from '../../../src/template/filter/filter'
|
||||
import Context from '../../../src/context/context'
|
||||
import { Filter } from '../../../src/template/filter/filter'
|
||||
import Value from '../../../src/template/value'
|
||||
|
||||
chai.use(sinonChai)
|
||||
@@ -12,56 +12,105 @@ const expect = chai.expect
|
||||
describe('Value', function () {
|
||||
beforeEach(() => Filter.clear())
|
||||
|
||||
it('should parse "foo', function () {
|
||||
const tpl: any = new Value('foo', false)
|
||||
expect(tpl.initial).to.equal('foo')
|
||||
expect(tpl.filters).to.deep.equal([])
|
||||
})
|
||||
|
||||
it('should parse "foo | add"', function () {
|
||||
const tpl: any = new Value('foo | add', false)
|
||||
expect(tpl.initial).to.equal('foo')
|
||||
expect(tpl.filters.length).to.equal(1)
|
||||
expect(tpl.filters[0].args).to.eql([])
|
||||
})
|
||||
it('should parse "foo | add: "foo" bar, 3"', function () {
|
||||
const tpl: any = new Value('foo | add: "foo" bar, 3', false)
|
||||
expect(tpl.initial).to.equal('foo')
|
||||
expect(tpl.filters.length).to.equal(1)
|
||||
expect(tpl.filters[0].name).to.eql('add')
|
||||
expect(tpl.filters[0].args).to.eql(['"foo"', '3'])
|
||||
})
|
||||
it('should parse "foo | add: 3, false"', function () {
|
||||
const tpl: any = new Value('foo | add: 3, "foo"', false)
|
||||
expect(tpl.initial).to.equal('foo')
|
||||
expect(tpl.filters.length).to.equal(1)
|
||||
expect(tpl.filters[0].args).to.eql(['3', '"foo"'])
|
||||
})
|
||||
it('should parse "foo | add: "|", 3', function () {
|
||||
const tpl: any = new Value('foo | add: "|", 3', false)
|
||||
expect(tpl.initial).to.equal('foo')
|
||||
expect(tpl.filters.length).to.equal(1)
|
||||
expect(tpl.filters[0].args).to.eql(['"|"', '3'])
|
||||
})
|
||||
|
||||
it('should parse "foo | add: "|", 3', function () {
|
||||
const tpl: any = new Value('foo | add: "|", 3', false)
|
||||
expect(tpl.initial).to.equal('foo')
|
||||
expect(tpl.filters.length).to.equal(1)
|
||||
expect(tpl.filters[0].args).to.eql(['"|"', '3'])
|
||||
})
|
||||
|
||||
it('should call chained filters correctly', function () {
|
||||
const date = sinon.stub().returns('y')
|
||||
const time = sinon.spy()
|
||||
Filter.register('date', date)
|
||||
Filter.register('time', time)
|
||||
const tpl = new Value('foo.bar | date: "b" | time:2', false)
|
||||
const scope = new Scope({
|
||||
foo: { bar: 'bar' }
|
||||
describe('#constructor()', function () {
|
||||
it('should parse "foo', function () {
|
||||
const tpl = new Value('foo', false) as any
|
||||
expect(tpl.initial).to.equal('foo')
|
||||
expect(tpl.filters).to.deep.equal([])
|
||||
})
|
||||
|
||||
it('should parse "foo | add"', function () {
|
||||
const tpl = new Value('foo | add', false) as any
|
||||
expect(tpl.initial).to.equal('foo')
|
||||
expect(tpl.filters.length).to.equal(1)
|
||||
expect(tpl.filters[0].args).to.eql([])
|
||||
})
|
||||
it('should parse "foo,foo | add"', function () {
|
||||
const tpl = new Value('foo,foo | add', false) as any
|
||||
expect(tpl.initial).to.equal('foo') as any
|
||||
expect(tpl.filters.length).to.equal(1)
|
||||
expect(tpl.filters[0].args).to.eql([])
|
||||
})
|
||||
it('should parse "foo | add: 3, false"', function () {
|
||||
const tpl = new Value('foo | add: 3, "foo"', false) as any
|
||||
expect(tpl.initial).to.equal('foo')
|
||||
expect(tpl.filters.length).to.equal(1)
|
||||
expect(tpl.filters[0].args).to.eql(['3', '"foo"'])
|
||||
})
|
||||
it('should parse "foo | add: "foo" bar, 3"', function () {
|
||||
const tpl = new Value('foo | add: "foo" bar, 3', false) as any
|
||||
expect(tpl.initial).to.equal('foo')
|
||||
expect(tpl.filters.length).to.equal(1)
|
||||
expect(tpl.filters[0].name).to.eql('add')
|
||||
expect(tpl.filters[0].args).to.eql(['"foo"', '3'])
|
||||
})
|
||||
it('should parse "foo | add: "|", 3', function () {
|
||||
const tpl = new Value('foo | add: "|", 3', false) as any
|
||||
expect(tpl.initial).to.equal('foo')
|
||||
expect(tpl.filters.length).to.equal(1)
|
||||
expect(tpl.filters[0].args).to.eql(['"|"', '3'])
|
||||
})
|
||||
it('should parse "foo | add: "|", 3', function () {
|
||||
const tpl = new Value('foo | add: "|", 3', false) as any
|
||||
expect(tpl.initial).to.equal('foo')
|
||||
expect(tpl.filters.length).to.equal(1)
|
||||
expect(tpl.filters[0].args).to.eql(['"|"', '3'])
|
||||
})
|
||||
it('should support arguments as named key/values', function () {
|
||||
const f = new Value('o | foo: key1: "literal1", key2: value2', false) as any
|
||||
expect(f.filters[0].name).to.equal('foo')
|
||||
expect(f.filters[0].args).to.eql([['key1', '"literal1"'], ['key2', 'value2']])
|
||||
})
|
||||
it('should support arguments as named key/values with inline literals', function () {
|
||||
const f = new Value('o | foo: "test0", key1: "literal1", key2: value2', false) as any
|
||||
expect(f.filters[0].name).to.equal('foo')
|
||||
expect(f.filters[0].args).to.deep.equal(['"test0"', ['key1', '"literal1"'], ['key2', 'value2']])
|
||||
})
|
||||
it('should support arguments as named key/values with inline values', function () {
|
||||
const f = new Value('o | foo: test0, key1: "literal1", key2: value2', false) as any
|
||||
expect(f.filters[0].name).to.equal('foo')
|
||||
expect(f.filters[0].args).to.deep.equal(['test0', ['key1', '"literal1"'], ['key2', 'value2']])
|
||||
})
|
||||
it('should support argument values named same as keys', function () {
|
||||
const f = new Value('o | foo: a: a', false) as any
|
||||
expect(f.filters[0].name).to.equal('foo')
|
||||
expect(f.filters[0].args).to.deep.equal([['a', 'a']])
|
||||
})
|
||||
it('should support argument literals named same as keys', function () {
|
||||
const f = new Value('o | foo: a: "a"', false) as any
|
||||
expect(f.filters[0].name).to.equal('foo')
|
||||
expect(f.filters[0].args).to.deep.equal([['a', '"a"']])
|
||||
})
|
||||
})
|
||||
|
||||
describe('#tokenize()', function () {
|
||||
it('should tokenize a simple value', function () {
|
||||
expect(Value.tokenize('foo')).to.eql(['foo'])
|
||||
})
|
||||
it('should tokenize a value with spaces', function () {
|
||||
expect(Value.tokenize(' foo \t')).to.eql(['foo'])
|
||||
})
|
||||
it('should tokenize a simple filter', function () {
|
||||
expect(Value.tokenize('foo | add')).to.eql(['foo', '|', 'add'])
|
||||
})
|
||||
it('should tokenize a filter with a single argument', function () {
|
||||
expect(Value.tokenize('foo | add: 1')).to.eql(['foo', '|', 'add', ':', '1'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('#value()', function () {
|
||||
it('should call chained filters correctly', async function () {
|
||||
const date = sinon.stub().returns('y')
|
||||
const time = sinon.spy()
|
||||
Filter.register('date', date)
|
||||
Filter.register('time', time)
|
||||
const tpl = new Value('foo.bar | date: "b" | time:2', false)
|
||||
const scope = new Context({
|
||||
foo: { bar: 'bar' }
|
||||
})
|
||||
await tpl.value(scope)
|
||||
expect(date).to.have.been.calledWith('bar', 'b')
|
||||
expect(time).to.have.been.calledWith('y', 2)
|
||||
})
|
||||
tpl.value(scope)
|
||||
expect(date).to.have.been.calledWith('bar', 'b')
|
||||
expect(time).to.have.been.calledWith('y', 2)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import * as chai from 'chai'
|
||||
import * as sinon from 'sinon'
|
||||
import * as sinonChai from 'sinon-chai'
|
||||
import * as chaiAsPromised from 'chai-as-promised'
|
||||
|
||||
const expect = chai.expect
|
||||
chai.use(sinonChai)
|
||||
chai.use(chaiAsPromised)
|
||||
|
||||
const P = require('../../../src/util/promise')
|
||||
|
||||
describe('util/promise', function () {
|
||||
describe('.mapSeries()', async function () {
|
||||
it('should resolve when all resolved', async function () {
|
||||
const result = await P.mapSeries(
|
||||
['first', 'second', 'third'],
|
||||
(item: string) => Promise.resolve(item)
|
||||
)
|
||||
return expect(result).to.deep.equal(['first', 'second', 'third'])
|
||||
})
|
||||
it('should reject with the error that first callback rejected', () => {
|
||||
const p = P.mapSeries(['first', 'second'],
|
||||
(item: string) => Promise.reject(item))
|
||||
return expect(p).to.rejectedWith('first')
|
||||
})
|
||||
it('should resolve in series', function () {
|
||||
const spy1 = sinon.spy()
|
||||
const spy2 = sinon.spy()
|
||||
return P
|
||||
.mapSeries(
|
||||
['first', 'second'],
|
||||
(item: string, idx: number) => new Promise(function (resolve) {
|
||||
if (idx === 0) {
|
||||
setTimeout(function () {
|
||||
spy1()
|
||||
resolve('first cb')
|
||||
}, 10)
|
||||
} else {
|
||||
spy2()
|
||||
resolve('foo')
|
||||
}
|
||||
}))
|
||||
.then(() => expect(spy2).to.have.been.calledAfter(spy1))
|
||||
})
|
||||
it('should not call rest of callbacks once rejected', () => {
|
||||
const spy = sinon.spy()
|
||||
return P
|
||||
.mapSeries(['first', 'second'], (item: string, idx: number) => {
|
||||
if (idx > 0) {
|
||||
spy()
|
||||
}
|
||||
return Promise.reject(new Error(item))
|
||||
})
|
||||
.catch(() => expect(spy).to.not.have.been.called)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -19,9 +19,6 @@ describe('util/underscore', function () {
|
||||
})
|
||||
})
|
||||
describe('.stringify()', function () {
|
||||
it('should respect to to_liquid() method', function () {
|
||||
expect(_.stringify({ to_liquid: () => 'foo' })).to.equal('foo')
|
||||
})
|
||||
it('should respect to toLiquid() method', function () {
|
||||
expect(_.stringify({ toLiquid: () => 'foo' })).to.equal('foo')
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"module":"CommonJS",
|
||||
"lib": ["es2015", "es2016", "es2017", "dom"],
|
||||
"sourceMap": true,
|
||||
"outDir": "dist",
|
||||
"declaration": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"resolveJsonModule": true,
|
||||
|
||||
Reference in New Issue
Block a user