mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-14 20:00:39 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea7e334d87 | ||
|
|
4f123c1d6b | ||
|
|
0f4edc5b53 | ||
|
|
48bee428d5 | ||
|
|
2d1f05799f | ||
|
|
dbc18539a0 | ||
|
|
492f56cbfb | ||
|
|
4018ffe68b | ||
|
|
21eefe1d7b | ||
|
|
0c8148c12b | ||
|
|
7f336ded5c | ||
|
|
5ecca1b761 | ||
|
|
af284daf34 | ||
|
|
21a5ac7e08 | ||
|
|
7d22214cb5 | ||
|
|
d6ca12f3bd | ||
|
|
84f7a399c5 | ||
|
|
5b5dfd5c00 | ||
|
|
d032f7f4d4 | ||
|
|
044035b8ed | ||
|
|
564d9cd9f4 | ||
|
|
fbb1dc8a36 | ||
|
|
c99dfd69e7 | ||
|
|
4719544d49 | ||
|
|
2c0ba9bfd9 | ||
|
|
89636736a4 | ||
|
|
deca9677dd | ||
|
|
543368ed89 | ||
|
|
dcd7b01fa4 | ||
|
|
209d3910ab | ||
|
|
40bdd7e94a | ||
|
|
03b7ee95ed | ||
|
|
64e9cb7044 |
+1
-1
@@ -1,6 +1,6 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "4"
|
||||
- "5"
|
||||
before_script:
|
||||
- npm install -g mocha
|
||||
after_script:
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# shopify-liquid
|
||||
|
||||
[](https://www.npmjs.org/package/shopify-liquid)
|
||||
[](https://www.npmjs.org/package/shopify-liquid)
|
||||
[](https://travis-ci.org/harttle/shopify-liquid)
|
||||
[](https://coveralls.io/github/harttle/shopify-liquid?branch=master)
|
||||
[](https://david-dm.org/harttle/shopify-liquid)
|
||||
[](https://coveralls.io/github/harttle/shopify-liquid?branch=master)
|
||||
[](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
|
||||
|
||||
Vendored
+61
-23
@@ -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', {
|
||||
|
||||
Vendored
+2
-2
File diff suppressed because one or more lines are too long
+2
-2
@@ -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, ''),
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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', {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
@@ -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
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
@@ -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
@@ -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+\)/);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user