Compare commits

...
33 Commits
Author SHA1 Message Date
semantic-release-bot 4282accaa2 chore(release): 8.2.1 [skip ci]
## [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))
2019-04-26 16:32:20 +00:00
harttle 6075c0a54f fix: default filter is not working with an empty string, #122 2019-04-27 00:26:17 +08:00
semantic-release-bot 87489f6098 chore(release): 8.2.0 [skip ci]
# [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))
2019-04-17 02:51:13 +00:00
harttle 6c5dc336e7 chore: fix linting, working on #120 2019-04-17 10:45:02 +08:00
harttle 00bc1efe6a feat: pass context to filters 2019-04-17 10:24:55 +08:00
harttle 6fb4796202 chore: set outDir to dist, see #119 2019-04-16 11:23:29 +08:00
semantic-release-bot 7483613b23 chore(release): 8.1.0 [skip ci]
# [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))
2019-04-02 04:55:18 +00:00
harttle 7762dd9612 chore: fix linting 2019-04-02 12:10:35 +08:00
Jun Yang daa0b574fc feat: where filter, working on #118
doc: https://shopify.github.io/liquid/filters/where/
ruby impl: https://github.com/Shopify/liquid/blob/14cd011cb5eef042af192ad5294c8f0e1ae89f06/lib/liquid/standardfilters.rb
2019-04-02 00:46:32 +08:00
semantic-release-bot a084183246 chore(release): 8.0.3 [skip ci]
## [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))
2019-04-01 03:04:24 +00:00
harttle eadb6f3711 fix: slice filter on negative begin, #117 2019-04-01 10:57:08 +08:00
semantic-release-bot dba26f24e6 chore(release): 8.0.2 [skip ci]
## [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))
2019-03-25 15:15:04 +00:00
Jun Yang 722da6a7ee doc: update README 2019-03-25 23:01:04 +08:00
harttle 82d7673554 perf: use polymophism instead duck test 2019-03-25 20:11:23 +08:00
harttle 64dd057552 refactor: Context#propertyAccessSeq => parseProp 2019-03-25 10:36:23 +08:00
harttle 45e3c2bb8e refactor: switch Context <-> Scope concepts 2019-03-25 10:36:23 +08:00
Jun Yang 76345a64c3 doc: update README.md 2019-03-23 22:28:33 +08:00
semantic-release-bot 9e69652655 chore(release): 8.0.1 [skip ci]
## [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))
2019-03-22 12:48:41 +00:00
harttle defbb58e66 fix: incorrect scope when using assign with for, fixes #115
* remove AssignScope, CaptureScope, IncrementScope, DecrementScope concepts
* introduce Scope.environments for decrement/increment
* assign to scopes[0] for assign/capture
2019-03-22 20:38:54 +08:00
ssendevandJun Yang b7b92c63cf async Register Tags example 2019-03-21 20:46:21 +08:00
ssendevandJun Yang 265534b7b7 Fix Register Tags example 2019-03-21 20:46:21 +08:00
semantic-release-bot 84ea2c7488 chore(release): 8.0.0 [skip ci]
# [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
2019-03-10 11:28:39 +00:00
Jun Yang 64e0c876e9 refactor: use camelCase for JavaScript APIs
BREAKING CHANGE: Options and method names in JavaScript API are now renamed to cammelCase, for a complete list see #109
2019-03-10 19:21:09 +08:00
Jun Yang 4a8088d4e4 feat: promise support for drops, working on #65 2019-03-10 18:05:52 +08:00
harttle ad0930f152 test: cover parseFilters in template/value.ts 2019-03-06 01:46:35 +08:00
harttle 998832c772 refactor: mv custom filter test 2019-03-06 01:40:31 +08:00
semantic-release-bot 4ba224043b chore(release): 7.5.1 [skip ci]
## [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))
2019-03-05 17:39:56 +00:00
harttle 5ffc904f80 fix: named params for filters, working on #113 2019-03-06 01:30:47 +08:00
semantic-release-bot 08646f75b0 chore(release): 7.5.0 [skip ci]
# [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))
2019-03-01 22:06:31 +00:00
Jun Yang 3647305dcf feat: tablerowloop object
See: https://www.rubydoc.info/gems/liquid/Liquid/TablerowloopDrop
2019-03-02 06:00:30 +08:00
Jun Yang cca0306f5b chore: fix mocha pattern for windows
See: https://github.com/mochajs/mocha/issues/2895
2019-03-02 03:57:38 +08:00
Jun Yang eeeacc4492 chore: fix rollup.config.ts 2019-03-02 03:42:57 +08:00
Jun Yang d44e1d727a chore: fix test on windows 10 2019-03-02 03:36:33 +08:00
81 changed files with 1266 additions and 1202 deletions
+73
View File
@@ -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)
+44 -58
View File
@@ -12,51 +12,50 @@
[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://github.com/harttle/liquidjs)
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](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
View File
@@ -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>`
}
})
Vendored
+2 -2
View File
@@ -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
}
+23 -6
View File
@@ -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
View File
@@ -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
View File
@@ -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' ],
+6 -2
View File
@@ -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)
}
}
+2 -2
View File
@@ -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 -3
View File
@@ -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}
}
+3 -7
View File
@@ -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
+8 -7
View File
@@ -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
+4 -7
View File
@@ -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
+6 -6
View File
@@ -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
+5 -5
View File
@@ -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)
}
}
+6 -16
View File
@@ -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
View File
@@ -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
}
}
+5 -5
View File
@@ -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
View File
@@ -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
}
}
+6 -16
View File
@@ -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
View File
@@ -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
+17 -27
View File
@@ -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
+5 -5
View File
@@ -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
+147
View File
@@ -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
}
+8
View File
@@ -0,0 +1,8 @@
import { Drop } from '../drop/drop'
type PlainObject = {
[key: string]: any
toLiquid?: () => any
}
export type Scope = PlainObject | Drop
+1 -1
View File
@@ -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
}
}
+34
View File
@@ -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)
}
}
+25
View File
@@ -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
}
}
-8
View File
@@ -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
View File
@@ -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) {
+16 -7
View File
@@ -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
}
}
+3
View File
@@ -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'
}
}
+15 -2
View File
@@ -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'
}
}
+3 -7
View File
@@ -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)
+3 -3
View File
@@ -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
View File
@@ -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'
}
}
+2
View File
@@ -1,4 +1,6 @@
export default class Token {
trimLeft: boolean = false
trimRight: boolean = false
type: string = 'notset'
line: number
col: number
+24 -19
View File
@@ -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
+12 -21
View File
@@ -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, '')
+4 -4
View File
@@ -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
View File
@@ -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
}
-174
View File
@@ -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
}
-4
View File
@@ -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
+5 -1
View File
@@ -1 +1,5 @@
export type FilterImpl = (value: any, ...args: any[]) => any
import Context from '../../context/context'
export interface FilterImpl {
context: Context
}
+18 -11
View File
@@ -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 () {
+2 -2
View File
@@ -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>;
}
+3 -3
View File
@@ -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)
}
}
+5 -3
View File
@@ -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
}
}
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
-7
View File
@@ -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
}
-19
View File
@@ -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)
}
-13
View File
@@ -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
}
+2 -2
View File
@@ -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/)
})
})
+27 -2
View File
@@ -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
`)
})
})
})
+7 -2
View File
@@ -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'))
})
})
+12 -7
View File
@@ -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')
})
})
})
+6 -15
View File
@@ -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}}'
+35 -3
View File
@@ -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)
})
})
})
+23 -2
View File
@@ -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 -1
View File
@@ -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 () {
+10 -10
View File
@@ -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
View File
@@ -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
View File
@@ -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) {
+187
View File
@@ -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')
})
})
})
+3 -3
View File
@@ -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
View File
@@ -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])
})
})
})
-272
View File
@@ -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')
})
})
+21 -15
View File
@@ -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 () {
+6 -20
View File
@@ -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
View File
@@ -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
View File
@@ -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)
})
})
-57
View File
@@ -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)
})
})
})
-3
View File
@@ -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')
})
+1
View File
@@ -4,6 +4,7 @@
"module":"CommonJS",
"lib": ["es2015", "es2016", "es2017", "dom"],
"sourceMap": true,
"outDir": "dist",
"declaration": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,