Compare commits

..
33 Commits
Author SHA1 Message Date
harttle ea7e334d87 1.7.2 2017-03-15 17:14:21 +08:00
harttle 4f123c1d6b Merge branch 'chenos-master' 2017-03-15 17:13:58 +08:00
harttle 0f4edc5b53 add unit test for #21 2017-03-15 17:13:05 +08:00
chenosandGitHub 48bee428d5 reversed position
eg:

- reversed limit:4 offset:2
- limit:4 reversed offset:2
- limit:4 offset:2 reversed
2017-03-15 16:59:24 +08:00
Jun YangandGitHub 2d1f05799f Update README.md 2017-02-18 17:14:51 +08:00
harttle dbc18539a0 modified: README.md 2017-02-14 10:57:57 +08:00
harttle 492f56cbfb unit test: multiple hash 2017-02-01 13:49:01 +08:00
harttle 4018ffe68b 1.7.1 2017-01-31 23:17:08 +08:00
harttle 21eefe1d7b do not trim output tags and previous \n, #19 2017-01-31 23:15:23 +08:00
harttle 0c8148c12b 1.7.0 2017-01-31 00:48:26 +08:00
harttle 7f336ded5c fix badges 2017-01-31 00:48:11 +08:00
harttle 5ecca1b761 feature: greedy trim disabled by default, working on #19 2017-01-31 00:07:46 +08:00
harttle af284daf34 1.6.2 2017-01-21 19:41:59 +08:00
harttle 21a5ac7e08 RenderError extends original error 2017-01-21 19:41:19 +08:00
harttle 7d22214cb5 remove strict_* from express arguments 2017-01-02 13:44:33 +08:00
harttle d6ca12f3bd 1.6.1 2016-12-27 00:46:53 +08:00
harttle 84f7a399c5 update README and unit test 2016-12-27 00:33:11 +08:00
harttle 5b5dfd5c00 documents for whitespace control (#17) 2016-12-27 00:09:39 +08:00
harttle d032f7f4d4 1.6.0 2016-12-26 23:38:15 +08:00
harttle 044035b8ed unit tests for #17 2016-12-26 23:37:33 +08:00
harttle 564d9cd9f4 feature: whitespace control, fixes #17 2016-12-26 23:23:58 +08:00
harttle fbb1dc8a36 test cases for filters: string->number convertion, #18 2016-12-15 23:14:49 +08:00
harttle c99dfd69e7 1.5.4 2016-12-15 22:39:52 +08:00
harttle 4719544d49 fix: number convertion for plus filter, closes #18 2016-12-15 22:38:48 +08:00
harttle 2c0ba9bfd9 1.5.3 2016-12-08 23:26:00 +08:00
harttle 89636736a4 robust date filter 2016-12-08 23:25:38 +08:00
harttle deca9677dd modified: README.md 2016-12-05 23:49:07 +08:00
harttle 543368ed89 unit test for original error for {%layout%} 2016-12-01 00:50:36 +08:00
harttle dcd7b01fa4 update tag test 2016-11-16 02:53:30 +08:00
harttle 209d3910ab 1.5.2 2016-11-16 01:07:08 +08:00
harttle 40bdd7e94a refactor: move file to token property 2016-11-16 01:06:37 +08:00
harttle 03b7ee95ed 1.5.1 2016-11-16 00:50:21 +08:00
harttle 64e9cb7044 fix nested render error message 2016-11-16 00:48:54 +08:00
17 changed files with 439 additions and 145 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
language: node_js
node_js:
- "4"
- "5"
before_script:
- npm install -g mocha
after_script:
+25 -20
View File
@@ -1,21 +1,21 @@
# shopify-liquid
[![NPM version](https://img.shields.io/npm/v/shopify-liquid.svg?style=flat)](https://www.npmjs.org/package/shopify-liquid)
[![npm](https://img.shields.io/npm/v/shopify-liquid.svg)](https://www.npmjs.org/package/shopify-liquid)
[![Build Status](https://travis-ci.org/harttle/shopify-liquid.svg?branch=master)](https://travis-ci.org/harttle/shopify-liquid)
[![Coverage Status](https://img.shields.io/coveralls/harttle/shopify-liquid/master.svg)](https://coveralls.io/github/harttle/shopify-liquid?branch=master)
[![Dependency manager](https://img.shields.io/david/harttle/shopify-liquid.svg?style=flat)](https://david-dm.org/harttle/shopify-liquid)
[![Coveralls](https://img.shields.io/coveralls/harttle/shopify-liquid.svg)](https://coveralls.io/github/harttle/shopify-liquid?branch=master)
[![GitHub issues](https://img.shields.io/github/issues-closed/harttle/shopify-liquid.svg)](https://github.com/harttle/shopify-liquid/issues)
A feature-rich Liquid template engine for Node.js and browsers,
with compliance with [the Ruby version][shopify-liquid].
New to Liquid? Here's a live demo: <http://harttle.com/shopify-liquid/>
A Liquid template engine with a wide range of tags and filters,
for both Node.js and browsers.
Here's a live demo: <http://harttle.com/shopify-liquid/>
> The Liquid template engine is implemented in Ruby originally,
> which is used by [Jekyll][jekyll] and [Github Pages][gh].
Features:
* Pretty much [builtin filters](https://github.com/harttle/shopify-liquid/wiki/Builtin-Filters) and [builtin tags](https://github.com/harttle/shopify-liquid/wiki/Builtin-Tags)
* Async rendering, especially for tags
* A wide range of [filters](https://github.com/harttle/shopify-liquid/wiki/Builtin-Filters) and [tags](https://github.com/harttle/shopify-liquid/wiki/Builtin-Tags)
* Easy tag/filter registration, allows async tags
* [any-promise][any-promise]
Installation:
@@ -52,11 +52,8 @@ engine.render(tpl, {name: 'alice'})
```javascript
var engine = Liquid({
root: path.resolve(__dirname, 'views/'), // for layouts and includes
extname: '.liquid',
cache: false,
strict_filters: false, // default: false
strict_variables: false // default: false
root: path.resolve(__dirname, 'views/'), // dirs to lookup layouts/includes
extname: '.liquid' // the default extname used for layouts/includes
});
engine.renderFile("hello.liquid", {name: 'alice'})
.then(function(html){
@@ -69,11 +66,15 @@ engine.renderFile("hello", {name: 'alice'})
});
```
## Options
The full list of options for `Liquid()` is listed as following:
* `root` is a directory or an array of directories to resolve layouts and includes, as well as the filename passed in when calling `.renderFile()`.
If an array, the files are looked up in the order they occur in the array.
Defaults to `process.cwd()`
Defaults to `["."]`
* `extname` is used to lookup the template file when filepath doesn't include an extension name. Defaults to `.liquid`
* `extname` is used to lookup the template file when filepath doesn't include an extension name. Defaults to `".liquid"`
* `cache` indicates whether or not to cache resolved templates. Defaults to `false`.
@@ -83,14 +84,17 @@ Defaults to `process.cwd()`
If set to `false`, undefined variables will be rendered as empty string.
Otherwise, undefined variables will cause an exception. Defaults to `false`.
* `trim_right` is used to strip blank characters (including ` `, `\t`, and `\r`) from the right of tags (`{% %}`) until `\n` (inclusive). Defaults to `false`.
* `trim_left` is similiar to `trim_right`, whereas the `\n` is exclusive. Defaults to `false`. See [Whitespace Control][whitespace control] for details.
* `greedy` is used to specify whether `trim_left`/`trim_right` is greedy. When set to `true`, all successive blank characters including `\n` will be trimed regardless of line breaks. Defaults to `false`.
## Use with Express.js
```javascript
// register liquid engine
app.engine('liquid', engine.express({
strict_variables: true, // Default: fasle
strict_filters: true // Default: false
}));
app.engine('liquid', engine.express());
app.set('views', './views'); // specify the views directory
app.set('view engine', 'liquid'); // set to default
```
@@ -205,7 +209,7 @@ engine.registerTag('upper', {
## Contribution Guide
1. Write a [test][test] to define the feature you want.
2. Just initiate an issue, or optionally:
2. File an issue, or optionally:
3. Get your test pass and make a pull request.
[nunjucks]: http://mozilla.github.io/nunjucks/
@@ -217,3 +221,4 @@ engine.registerTag('upper', {
[any-promise]: https://github.com/kevinbeaty/any-promise
[test]: https://github.com/harttle/shopify-liquid/tree/master/test
[caniuse-promises]: http://caniuse.com/#feat=promises
[whitespace control]: https://github.com/harttle/shopify-liquid/wiki/Whitespace-Control
+61 -23
View File
@@ -34,7 +34,7 @@ var filters = {
},
'date': function date(v, arg) {
if (v === 'now') v = new Date();
return strftime(v, arg);
return v instanceof Date ? strftime(v, arg) : '';
},
'default': function _default(v, arg) {
return v || arg;
@@ -80,7 +80,7 @@ var filters = {
return v.replace(/\n/g, '<br />');
},
'plus': bindFixed(function (v, arg) {
return v + arg;
return Number(v) + Number(arg);
}),
'prepend': function prepend(v, arg) {
return arg + v;
@@ -241,8 +241,8 @@ var _engine = {
return this;
},
parse: function parse(html) {
var tokens = tokenizer.parse(html);
parse: function parse(html, filepath) {
var tokens = tokenizer.parse(html, filepath, this.options);
return this.parser.parse(tokens);
},
render: function render(tpl, ctx, opts) {
@@ -270,9 +270,6 @@ var _engine = {
opts = _.assign({}, opts);
return this.getTemplate(filepath, opts.root).then(function (templates) {
return _this2.render(templates, ctx, opts);
}).catch(function (e) {
e.file = filepath;
throw e;
});
},
evalOutput: function evalOutput(str, scope) {
@@ -321,7 +318,7 @@ var _engine = {
});
} else {
return readFileAsync(filepath).then(function (str) {
return _this3.parse(str);
return _this3.parse(str, filepath);
});
}
});
@@ -342,14 +339,18 @@ var _engine = {
};
function factory(options) {
options = _.assign({}, options);
options = _.assign({
root: ['.'],
cache: false,
extname: '.liquid',
trim_right: false,
trim_left: false,
strict_filters: false,
strict_variables: false
}, options);
options.root = normalizeStringArray(options.root);
if (!options.root.length) options.root = ['.'];
options.extname = options.extname || '.liquid';
var engine = Object.create(_engine);
engine.init(Tag(), Filter(options), options);
return engine;
}
@@ -1198,11 +1199,13 @@ var TokenizationError = require('./util/error.js').TokenizationError;
var _ = require('./util/underscore.js');
var assert = require('../src/util/assert.js');
function parse(html) {
function parse(html, filepath, options) {
assert(_.isString(html), 'illegal input type');
html = whiteSpaceCtrl(html, options);
var tokens = [];
var syntax = /({%(.*?)%})|({{(.*?)}})/g;
var syntax = /({%-?(.*?)-?%})|({{(.*?)}})/g;
var result, htmlFragment, token;
var lastMatchEnd = 0,
lastMatchBegin = -1,
@@ -1256,7 +1259,8 @@ function parse(html) {
raw: match[offset],
value: match[offset + 1].trim(),
line: getLineNum(match),
input: html
input: html,
file: filepath
};
}
@@ -1268,7 +1272,21 @@ function parse(html) {
}
}
function whiteSpaceCtrl(html, options) {
options = options || {};
if (options.trim_left) {
html = html.replace(/{%-?/g, '{%-');
}
if (options.trim_right) {
html = html.replace(/-?%}/g, '-%}');
}
var rLeft = options.greedy ? /\s+({%-)/g : /[\t\r ]*({%-)/g;
var rRight = options.greedy ? /(-%})\s+/g : /(-%})[\t\r ]*\n?/g;
return html.replace(rLeft, '$1').replace(rRight, '$1');
}
exports.parse = parse;
exports.whiteSpaceCtrl = whiteSpaceCtrl;
},{"../src/util/assert.js":16,"./lexical.js":8,"./util/error.js":17,"./util/underscore.js":21}],16:[function(require,module,exports){
'use strict';
@@ -1300,37 +1318,46 @@ function TokenizationError(message, token) {
this.input = token.input;
this.line = token.line;
this.file = token.file;
var context = mkContext(token.input, token.line);
this.message = message + ', line:' + token.line;
this.message = mkMessage(message, token);
this.stack = context + '\n' + (this.stack || '');
}
TokenizationError.prototype = Object.create(Error.prototype);
TokenizationError.prototype.constructor = TokenizationError;
function ParseError(e, token) {
_.assign(this, e);
this.originalError = e;
this.name = this.constructor.name;
this.stack = e.stack;
this.input = token.input;
this.line = token.line;
this.file = token.file;
var context = mkContext(token.input, token.line);
this.message = e.message + ', line:' + token.line;
this.stack = context + '\n' + (this.stack || '');
this.message = mkMessage(e.message || 'Unkown Error', token);
this.stack = context + '\n' + (e.stack || '');
}
ParseError.prototype = Object.create(Error.prototype);
ParseError.prototype.constructor = ParseError;
function RenderError(e, tpl) {
// return the original render error
if (e instanceof RenderError) {
return e;
}
_.assign(this, e);
this.originalError = e;
this.name = this.constructor.name;
this.stack = e.stack;
this.input = tpl.token.input;
this.line = tpl.token.line;
this.file = tpl.token.file;
var context = mkContext(tpl.token.input, tpl.token.line);
this.message = e.message + ', line:' + tpl.token.line;
this.message = mkMessage(e.message || 'Unkown Error', tpl.token);
this.stack = context + '\n' + (e.stack || '');
}
RenderError.prototype = Object.create(Error.prototype);
@@ -1375,6 +1402,17 @@ function align(n, max) {
return blank + str;
}
function mkMessage(msg, token) {
msg = msg || '';
if (token.file) {
msg += ', file:' + token.file;
}
if (token.line) {
msg += ', line:' + token.line;
}
return msg;
}
module.exports = {
TokenizationError: TokenizationError,
ParseError: ParseError,
@@ -1998,7 +2036,7 @@ var lexical = Liquid.lexical;
var mapSeries = require('../src/util/promise.js').mapSeries;
var RenderBreakError = Liquid.Types.RenderBreakError;
var assert = require('../src/util/assert.js');
var re = new RegExp('^(' + lexical.identifier.source + ')\\s+in\\s+' + ('(' + lexical.value.source + ')') + ('(?:\\s+' + lexical.hash.source + ')*') + '(?:\\s+(reversed))?$');
var re = new RegExp('^(' + lexical.identifier.source + ')\\s+in\\s+' + ('(' + lexical.value.source + ')') + ('(?:\\s+' + lexical.hash.source + ')*') + '(?:\\s+(reversed))?' + ('(?:\\s+' + lexical.hash.source + ')*$'));
module.exports = function (liquid) {
liquid.registerTag('for', {
+2 -2
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -23,7 +23,7 @@ var filters = {
'ceil': v => Math.ceil(v),
'date': (v, arg) => {
if (v === 'now') v = new Date();
return strftime(v, arg);
return v instanceof Date ? strftime(v, arg) : '';
},
'default': (v, arg) => v || arg,
'divided_by': (v, arg) => Math.floor(v / arg),
@@ -40,7 +40,7 @@ var filters = {
'minus': bindFixed((v, arg) => v - arg),
'modulo': bindFixed((v, arg) => v % arg),
'newline_to_br': v => v.replace(/\n/g, '<br />'),
'plus': bindFixed((v, arg) => v + arg),
'plus': bindFixed((v, arg) => Number(v) + Number(arg)),
'prepend': (v, arg) => arg + v,
'remove': (v, arg) => v.split(arg).join(''),
'remove_first': (v, l) => v.replace(l, ''),
+13 -13
View File
@@ -33,8 +33,8 @@ var _engine = {
return this;
},
parse: function(html) {
var tokens = tokenizer.parse(html);
parse: function(html, filepath) {
var tokens = tokenizer.parse(html, filepath, this.options);
return this.parser.parse(tokens);
},
render: function(tpl, ctx, opts) {
@@ -56,11 +56,7 @@ var _engine = {
renderFile: function(filepath, ctx, opts) {
opts = _.assign({}, opts);
return this.getTemplate(filepath, opts.root)
.then(templates => this.render(templates, ctx, opts))
.catch(e => {
e.file = filepath;
throw e;
});
.then(templates => this.render(templates, ctx, opts));
},
evalOutput: function(str, scope) {
var tpl = this.parser.parseOutput(str.trim());
@@ -100,7 +96,7 @@ var _engine = {
.then(str => this.parse(str))
.then(tpl => this.cache[filepath] = tpl);
} else {
return readFileAsync(filepath).then(str => this.parse(str));
return readFileAsync(filepath).then(str => this.parse(str, filepath));
}
});
},
@@ -119,14 +115,18 @@ var _engine = {
};
function factory(options) {
options = _.assign({}, options);
options = _.assign({
root: ['.'],
cache: false,
extname: '.liquid',
trim_right: false,
trim_left: false,
strict_filters: false,
strict_variables: false
}, options);
options.root = normalizeStringArray(options.root);
if (!options.root.length) options.root = ['.'];
options.extname = options.extname || '.liquid';
var engine = Object.create(_engine);
engine.init(Tag(), Filter(options), options);
return engine;
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "shopify-liquid",
"version": "1.5.0",
"version": "1.7.2",
"description": "A feature-rich Liquid template engine for Node.js and browsers, with compliance with the Ruby version.",
"main": "index.js",
"scripts": {
@@ -35,7 +35,7 @@
"babelify": "^7.3.0",
"browserify": "^13.1.0",
"chai": "^3.5.0",
"chai-as-promised": "^5.3.0",
"chai-as-promised": "^6.0.0",
"coveralls": "^2.11.9",
"express": "^4.14.0",
"istanbul": "^0.4.3",
+22 -3
View File
@@ -3,11 +3,13 @@ const TokenizationError = require('./util/error.js').TokenizationError;
const _ = require('./util/underscore.js');
const assert = require('../src/util/assert.js');
function parse(html) {
function parse(html, filepath, options) {
assert(_.isString(html), 'illegal input type');
html = whiteSpaceCtrl(html, options);
var tokens = [];
var syntax = /({%(.*?)%})|({{(.*?)}})/g;
var syntax = /({%-?(.*?)-?%})|({{(.*?)}})/g;
var result, htmlFragment, token;
var lastMatchEnd = 0, lastMatchBegin = -1, parsedLinesCount = 0;
@@ -58,7 +60,8 @@ function parse(html) {
raw: match[offset],
value: match[offset + 1].trim(),
line: getLineNum(match),
input: html
input: html,
file: filepath
};
}
@@ -70,4 +73,20 @@ function parse(html) {
}
}
function whiteSpaceCtrl(html, options){
options = options || {};
if(options.trim_left) {
html = html.replace(/{%-?/g, '{%-');
}
if(options.trim_right) {
html = html.replace(/-?%}/g, '-%}');
}
var rLeft = options.greedy ? /\s+({%-)/g : /[\t\r ]*({%-)/g;
var rRight = options.greedy ? /(-%})\s+/g : /(-%})[\t\r ]*\n?/g;
return html.replace(rLeft, '$1').replace(rRight, '$1');
}
exports.parse = parse;
exports.whiteSpaceCtrl = whiteSpaceCtrl;
+26 -6
View File
@@ -8,37 +8,46 @@ function TokenizationError(message, token) {
this.input = token.input;
this.line = token.line;
this.file = token.file;
var context = mkContext(token.input, token.line);
this.message = message + ', line:' + token.line;
this.message = mkMessage(message, token);
this.stack = context + '\n' + (this.stack || '');
}
TokenizationError.prototype = Object.create(Error.prototype);
TokenizationError.prototype.constructor = TokenizationError;
function ParseError(e, token) {
_.assign(this, e);
this.originalError = e;
this.name = this.constructor.name;
this.stack = e.stack;
this.input = token.input;
this.line = token.line;
this.file = token.file;
var context = mkContext(token.input, token.line);
this.message = e.message + ', line:' + token.line;
this.stack = context + '\n' + (this.stack || '');
this.message = mkMessage(e.message || 'Unkown Error', token);
this.stack = context + '\n' + (e.stack || '');
}
ParseError.prototype = Object.create(Error.prototype);
ParseError.prototype.constructor = ParseError;
function RenderError(e, tpl) {
// return the original render error
if(e instanceof RenderError){
return e;
}
_.assign(this, e);
this.originalError = e;
this.name = this.constructor.name;
this.stack = e.stack;
this.input = tpl.token.input;
this.line = tpl.token.line;
this.file = tpl.token.file;
var context = mkContext(tpl.token.input, tpl.token.line);
this.message = e.message + ', line:' + tpl.token.line;
this.message = mkMessage(e.message || 'Unkown Error', tpl.token);
this.stack = context + '\n' + (e.stack || '');
}
RenderError.prototype = Object.create(Error.prototype);
@@ -89,6 +98,17 @@ function align(n, max) {
return blank + str;
}
function mkMessage(msg, token){
msg = msg || '';
if(token.file){
msg += ', file:' + token.file;
}
if(token.line){
msg += ', line:' + token.line;
}
return msg;
}
module.exports = {
TokenizationError,
ParseError,
+2 -1
View File
@@ -6,7 +6,8 @@ const assert = require('../src/util/assert.js');
const re = new RegExp(`^(${lexical.identifier.source})\\s+in\\s+` +
`(${lexical.value.source})` +
`(?:\\s+${lexical.hash.source})*` +
`(?:\\s+(reversed))?$`);
`(?:\\s+(reversed))?` +
`(?:\\s+${lexical.hash.source})*$`);
module.exports = function(liquid) {
liquid.registerTag('for', {
+12
View File
@@ -26,6 +26,7 @@ describe('filters', function() {
describe('abs', function() {
it('should return 3 for -3', () => test('{{ -3 | abs }}', '3'));
it('should return 2 for arr[0]', () => test('{{ arr[0] | abs }}', '2'));
it('should return convert string', () => test('{{ "-3" | abs }}', '3'));
});
describe('append', function() {
@@ -51,6 +52,9 @@ describe('filters', function() {
it('should create a new Date when given "now"', function() {
return test('{{ "now" | date: "%Y"}}', (new Date).getFullYear().toString());
});
it('should render as empty string when invalid', function() {
return test('{{ "" | date: "%Y"}}', "");
});
});
describe('default', function() {
@@ -62,6 +66,7 @@ describe('filters', function() {
it('should return 2 for 4,2', () => test('{{4 | divided_by: 2}}', '2'));
it('should return 4 for 16,4', () => test('{{16 | divided_by: 4}}', '4'));
it('should return 1 for 5,3', () => test('{{5 | divided_by: 3}}', '1'));
it('should convert string to number', () => test('{{"5" | divided_by: "3"}}', '1'));
});
describe('downcase', function() {
@@ -130,6 +135,8 @@ describe('filters', function() {
it('should return "12" for 16,4', () => test('{{ 16 | minus: 4 }}', '12'));
it('should return "171.357" for 183.357,12',
() => test('{{ 183.357 | minus: 12 }}', '171.357'));
it('should convert first arg as number', () => test('{{ "4" | minus: 1 }}', '3'));
it('should convert both args as number', () => test('{{ "4" | minus: "1" }}', '3'));
});
describe('modulo', function() {
@@ -137,6 +144,7 @@ describe('filters', function() {
it('should return "3" for 24,7', () => test('{{ 24 | modulo: 7 }}', '3'));
it('should return "3.357" for 183.357,12',
() => test('{{ 183.357 | modulo: 12 }}', '3.357'));
it('should convert string', () => test('{{ "24" | modulo: "7" }}', '3'));
});
it('should support string_with_newlines', function() {
@@ -156,6 +164,8 @@ describe('filters', function() {
it('should return "20" for 16,4', () => test('{{ 16 | plus: 4 }}', '20'));
it('should return "195.357" for 183.357,12',
() => test('{{ 183.357 | plus: 12 }}', '195.357'));
it('should convert first arg as number', () => test('{{ "4" | plus: 2 }}', '6'));
it('should convert both args as number', () => test('{{ "4" | plus: "2" }}', '6'));
});
it('should support prepend', function() {
@@ -195,6 +205,7 @@ describe('filters', function() {
it('should return "3" for 2.7', () => test('{{2.7|round}}', '3'));
it('should return "183.36" for 183.357,2',
() => test('{{183.357|round: 2}}', '183.36'));
it('should convert string to number', () => test('{{"2.7"|round}}', '3'));
});
it('should support rstrip', function() {
@@ -261,6 +272,7 @@ describe('filters', function() {
it('should return "168" for 24,7', () => test('{{ 24 | times: 7 }}', '168'));
it('should return "2200.284" for 183.357,12',
() => test('{{ 183.357 | times: 12 }}', '2200.284'));
it('should convert string to number', () => test('{{ "24" | times: "7" }}', '168'));
});
describe('truncate', function() {
+49
View File
@@ -174,4 +174,53 @@ describe('liquid', function() {
.then(x => expect(x).to.equal('foo'));
});
});
describe('trim_left, trim_right', function() {
it('should trim_left for tags when trim_left=true', function() {
engine = Liquid({
trim_left: true
});
return engine.parseAndRender(' \n \t{%if true%}foo{%endif%} ')
.should.eventually.equal(' \nfoo ');
});
it('should trim_right for tags when trim_right=true', function() {
engine = Liquid({
trim_right: true
});
return engine.parseAndRender('\t{%if true%}foo{%endif%} \n')
.should.eventually.equal('\tfoo');
});
it('should trim all blanks before and after when greedy=true', function() {
engine = Liquid({
greedy: true
});
return engine.parseAndRender('\t{%-if true%}foo{%endif-%} \n \n')
.should.eventually.equal('foo');
});
it('should support trim using markup', function() {
engine = Liquid();
var src = [
'{%- assign username = "John G. Chalmers-Smith" -%}',
'{%- if username and username.length > 10 -%}',
' Wow, {{ username }}, you have a long name!',
'{%- else -%}',
' Hello there!',
'{%- endif -%}\n',
].join('\n');
var dst = ' Wow, John G. Chalmers-Smith, you have a long name!\n';
return engine.parseAndRender(src).should.eventually.equal(dst);
});
it('should not trim when not specified', function() {
engine = Liquid();
var src = [
'{% assign username = "John G. Chalmers-Smith" %}',
'{% if username and username.length > 10 %}',
' Wow, {{ username }}, you have a long name!',
'{% else %}',
' Hello there!',
'{% endif %}\n',
].join('\n');
var dst = '\n\n Wow, John G. Chalmers-Smith, you have a long name!\n\n';
return engine.parseAndRender(src).should.eventually.equal(dst);
});
});
});
+46 -20
View File
@@ -11,7 +11,10 @@ describe('tag', function() {
before(function() {
scope = Scope.factory({
foo: 'bar',
arr: [2, 1]
arr: [2, 1],
bar: {
coo: 'uoo'
}
});
tag.clear();
});
@@ -36,8 +39,7 @@ describe('tag', function() {
});
it('should call tag.render', function() {
var spy = sinon.spy(),
tokens = [];
var spy = sinon.spy();
tag.register('foo', {
render: spy
});
@@ -51,23 +53,47 @@ describe('tag', function() {
.then(() => expect(spy).to.have.been.called);
});
it('should call tag.render with resolved hash', function() {
var spy = sinon.spy(),
tokens = [];
tag.register('foo', {
render: spy
describe('hash', function(){
var spy, token;
beforeEach(function(){
spy = sinon.spy();
tag.register('foo', {
render: spy
});
token = {
type: 'tag',
value: 'foo aa:foo bb: arr[0] cc: 2.3 dd:bar.coo',
name: 'foo',
args: 'aa:foo bb: arr[0] cc: 2.3 dd:bar.coo'
};
});
it('should call tag.render with scope', function() {
return tag.construct(token, []).render(scope, {})
.then(() => expect(spy).to.have.been.calledWithMatch(scope));
});
it('should resolve identifier hash', function() {
return tag.construct(token, []).render(scope, {})
.then(() => expect(spy).to.have.been.calledWithMatch({}, {
aa: 'bar'
}));
});
it('should accept space between key/value', function() {
return tag.construct(token, []).render(scope, {})
.then(() => expect(spy).to.have.been.calledWithMatch({}, {
bb: 2,
}));
});
it('should resolve number value hash', function() {
return tag.construct(token, []).render(scope, {})
.then(() => expect(spy).to.have.been.calledWithMatch(scope, {
cc: 2.3
}));
});
it('should resolve property access hash', function() {
return tag.construct(token, []).render(scope, {})
.then(() => expect(spy).to.have.been.calledWithMatch(scope, {
dd: 'uoo'
}));
});
var token = {
type: 'tag',
value: 'foo aa:foo bb: arr[0] cc: 2.3',
name: 'foo',
args: 'aa:foo bb: arr[0] cc: 2.3'
};
return tag.construct(token, []).render(scope, {})
.then(() => expect(spy).to.have.been.calledWithMatch(scope, {
aa: 'bar',
bb: 2,
cc: 2.3
}));
});
});
+13 -1
View File
@@ -74,9 +74,21 @@ describe('tags/for', function() {
.to.eventually.equal('67');
});
it('should support for reversed', function() {
it('should support for reversed in the last position', function() {
var src = '{% for i in (1..5) limit:2 reversed %}{{ i }}{% endfor %}';
return expect(liquid.parseAndRender(src, ctx))
.to.eventually.equal('21');
});
it('should support for reversed in the middle position', function() {
var src = '{% for i in (1..5) reversed limit:2 %}{{ i }}{% endfor %}';
return expect(liquid.parseAndRender(src, ctx))
.to.eventually.equal('21');
});
it('should support for reversed in the first position', function() {
var src = '{% for i in (1..5) offset:2 reversed limit:4 %}{{ i }}{% endfor %}';
return expect(liquid.parseAndRender(src, ctx))
.to.eventually.equal('543');
});
});
+8
View File
@@ -78,4 +78,12 @@ describe('tags/layout', function() {
return expect(liquid.renderFile('/main.html')).to.
eventually.equal('blackA');
});
it('should support multiple hash', function() {
mock({
'/parent.html': '{{color}}{{bg}}{%block%}{%endblock%}',
'/main.html': '{% layout "parent.html" color:"black", bg:"red"%}{%block%}A{%endblock%}'
});
return expect(liquid.renderFile('/main.html')).to.
eventually.equal('blackredA');
});
});
+90 -46
View File
@@ -1,57 +1,101 @@
var chai = require("chai");
var should = chai.should();
var expect = chai.expect;
const chai = require("chai");
const parse = require('../src/tokenizer.js').parse;
const whiteSpaceCtrl = require('../src/tokenizer.js').whiteSpaceCtrl;
var tokenizer = require('../src/tokenizer.js');
const should = chai.should();
const expect = chai.expect;
describe('tokenizer', function() {
it('should handle plain HTML', function() {
var html = '<html><body><p>Lorem Ipsum</p></body></html>';
var tokens = tokenizer.parse(html);
describe('parse', function() {
it('should handle plain HTML', function() {
var html = '<html><body><p>Lorem Ipsum</p></body></html>';
var tokens = parse(html);
tokens.length.should.equal(1);
tokens[0].value.should.equal(html);
tokens[0].type.should.equal('html');
});
it('should throw when non-string passed in', function() {
expect(function() {
tokenizer.parse({});
}).to.throw('illegal input type');
});
it('should handle tag syntax', function() {
var html = '<p>{% for p in a[1]%}</p>';
var tokens = tokenizer.parse(html);
tokens.length.should.equal(1);
tokens[0].value.should.equal(html);
tokens[0].type.should.equal('html');
});
it('should throw when non-string passed in', function() {
expect(function() {
parse({});
}).to.throw('illegal input type');
});
it('should handle tag syntax', function() {
var html = '<p>{% for p in a[1]%}</p>';
var tokens = parse(html);
tokens.length.should.equal(3);
tokens[1].type.should.equal('tag');
tokens[1].value.should.equal('for p in a[1]');
});
it('should handle output syntax', function() {
var html = '<p>{{foo | date: "%Y-%m-%d"}}</p>';
var tokens = tokenizer.parse(html);
tokens.length.should.equal(3);
tokens[1].type.should.equal('tag');
tokens[1].value.should.equal('for p in a[1]');
});
it('should handle output syntax', function() {
var html = '<p>{{foo | date: "%Y-%m-%d"}}</p>';
var tokens = parse(html);
tokens.length.should.equal(3);
tokens[1].type.should.equal('output');
tokens[1].value.should.equal('foo | date: "%Y-%m-%d"');
});
it('should handle successive outputs and tags', function() {
var html = '{{foo}}{{bar}}{%foo%}{%bar%}';
var tokens = tokenizer.parse(html);
tokens.length.should.equal(3);
tokens[1].type.should.equal('output');
tokens[1].value.should.equal('foo | date: "%Y-%m-%d"');
});
it('should handle successive outputs and tags', function() {
var html = '{{foo}}{{bar}}{%foo%}{%bar%}';
var tokens = parse(html);
tokens.length.should.equal(4);
tokens[0].type.should.equal('output');
tokens[3].type.should.equal('tag');
tokens.length.should.equal(4);
tokens[0].type.should.equal('output');
tokens[3].type.should.equal('tag');
tokens[1].value.should.equal('bar');
tokens[2].value.should.equal('foo');
tokens[1].value.should.equal('bar');
tokens[2].value.should.equal('foo');
});
it('should keep white spaces and newlines', function() {
var html = '{%foo%}\n{%bar %} \n {%alice%}';
var tokens = parse(html);
expect(tokens.length).to.equal(5);
expect(tokens[1].type).to.equal('html');
expect(tokens[1].raw).to.equal('\n');
expect(tokens[3].type).to.equal('html');
expect(tokens[3].raw).to.equal(' \n ');
});
});
it('should keep white spaces and newlines', function() {
var html = '{{foo}}\n{%bar %} \n {{alice}}';
var tokens = tokenizer.parse(html);
expect(tokens.length).to.equal(5);
expect(tokens[1].type).to.equal('html');
expect(tokens[1].raw).to.equal('\n');
expect(tokens[3].type).to.equal('html');
expect(tokens[3].raw).to.equal(' \n ');
describe('whitespace control', function() {
it('should not strip by default', function() {
expect(whiteSpaceCtrl('\n {%foo%} \n')).to.equal('\n {%foo%} \n');
});
it('should strip all blank characters before and after', function() {
expect(whiteSpaceCtrl(' \t\r{%-foo-%} \t\n')).to.equal('{%-foo-%}');
});
it('should not trim previous/next lines', function() {
expect(whiteSpaceCtrl(' \t\n {%-foo-%}')).to.equal(' \t\n{%-foo-%}');
expect(whiteSpaceCtrl('{%-foo-%} \n \tfoo')).to.equal('{%-foo-%} \tfoo');
});
it('should trim exactly one trailing CR', function() {
expect(whiteSpaceCtrl('{%-foo-%} \n\n')).to.equal('{%-foo-%}\n');
});
it('should trim all leading/trailing blanks when options.greedy set', function() {
expect(whiteSpaceCtrl(' \n \n\t\r{%-foo-%}\n \n', {
greedy: true
})).to.equal('{%-foo-%}');
});
it('should strip whitespaces when set trim_left', function() {
expect(whiteSpaceCtrl('\n {%foo%} \n', {
trim_left: true
})).to.equal('\n{%-foo%} \n');
});
it('should strip whitespaces when set trim_right', function() {
expect(whiteSpaceCtrl('\n {%foo%} \n', {
trim_right: true
})).to.equal('\n {%foo-%}');
});
it('markup should has priority over options', function() {
expect(whiteSpaceCtrl('\n {%-foo%} \n', {
trim_left: false
})).to.equal('\n{%-foo%} \n');
});
it('should support a mix of markup and options', function() {
expect(whiteSpaceCtrl(' {%-foo%} \n', {
trim_left: true,
trim_right: true
})).to.equal('{%-foo-%}');
});
});
});
+65 -5
View File
@@ -10,6 +10,9 @@ var strictEngine = require('../..')({
});
describe('error', function() {
afterEach(function() {
mock.restore();
});
describe('TokenizationError', function() {
it('should throw TokenizationError when tag illegal', function() {
@@ -78,7 +81,9 @@ describe('error', function() {
describe('RenderError', function() {
beforeEach(function() {
engine = require('../..')();
engine = require('../..')({
root: '/'
});
engine.registerTag('throwingTag', {
render: function() {
throw new Error('intended render error');
@@ -131,7 +136,7 @@ describe('error', function() {
expect(e.message).to.contain('undefined variable: a');
});
});
it('should contain template content in err.message', function() {
it('should contain template context in err.stack', function() {
var html = ['1st', '2nd', '3rd', 'X{%throwingTag%} Y', '5th', '6th', '7th'];
var message = [
' 2| 2nd',
@@ -150,6 +155,61 @@ describe('error', function() {
expect(err.name).to.equal('RenderError');
});
});
it('should contain original error info for {% layout %}', function() {
mock({
'/throwing-tag.html': [
'1st',
'2nd',
'3rd',
'X{%throwingTag%} Y',
'5th',
'{%block%}{%endblock%}',
'7th'
].join('\n')
});
var html = '{%layout "throwing-tag.html"%}';
var message = [
' 2| 2nd',
' 3| 3rd',
'>> 4| X{%throwingTag%} Y',
' 5| 5th',
' 6| {%block%}{%endblock%}',
' 7| 7th',
'Error: intended render error',
];
return expect(engine.parseAndRender(html)).to.eventually
.be.rejected
.then(function(err) {
console.log(err.message);
console.log(err.stack);
expect(err.message).to.equal('intended render error, file:/throwing-tag.html, line:4');
expect(err.stack).to.contain(message.join('\n'));
expect(err.name).to.equal('RenderError');
});
});
it('should contain original error info for {% include %}', function() {
var origin = ['1st', '2nd', '3rd', 'X{%throwingTag%} Y', '5th', '6th', '7th'];
mock({
'/throwing-tag.html': origin.join('\n')
});
var html = '{%include "throwing-tag.html"%}';
var message = [
' 2| 2nd',
' 3| 3rd',
'>> 4| X{%throwingTag%} Y',
' 5| 5th',
' 6| 6th',
' 7| 7th',
'Error: intended render error',
];
return expect(engine.parseAndRender(html)).to.eventually
.be.rejected
.then(function(err) {
expect(err.message).to.equal('intended render error, file:/throwing-tag.html, line:4');
expect(err.stack).to.contain(message.join('\n'));
expect(err.name).to.equal('RenderError');
});
});
it('should contain the whole template content in err.input', function() {
var html = 'bar\nfoo{%throwingTag%}\nfoo';
return expect(engine.parseAndRender(html)).to.eventually
@@ -173,7 +233,7 @@ describe('error', function() {
.be.rejected
.then(function(err) {
expect(err.stack).to.contain('intended render reject');
expect(err.stack).to.contain('at Object.engine.registerTag.render');
expect(err.stack).to.match(/at .*:\d+:\d+\)/);
});
});
@@ -245,7 +305,7 @@ describe('error', function() {
});
});
it('should contain template content in err.message', function() {
it('should contain template context in err.stack', function() {
var html = ['1st', '2nd', '3rd', 'X{% a %} {% enda %} Y', '5th', '6th', '7th'];
var message = [
' 2| 2nd',
@@ -305,7 +365,7 @@ describe('error', function() {
.be.rejected
.then(function(err) {
expect(err.stack).to.contain('AssertionError: tag -a not found');
expect(err.stack).to.contain('at Object._tagInstance.parse');
expect(err.stack).to.match(/at .*:\d+:\d+\)/);
});
});