cover all

This commit is contained in:
harttle
2017-08-14 22:07:08 +08:00
parent 7904f823a8
commit 72597c52e5
14 changed files with 268 additions and 205 deletions
+1 -1
View File
@@ -4,4 +4,4 @@ node_js:
before_script: before_script:
- npm install -g mocha - npm install -g mocha
after_script: after_script:
- NODE_ENV=test ./node_modules/.bin/istanbul cover --report lcovonly ./node_modules/mocha/bin/_mocha -- -R spec --recursive && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage - npm run lcov && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage
+1 -6
View File
@@ -43,15 +43,10 @@ var _engine = {
return this.renderer.renderTemplates(tpl, scope) return this.renderer.renderTemplates(tpl, scope)
}, },
parseAndRender: function (html, ctx, opts) { parseAndRender: function (html, ctx, opts) {
console.log('parse and render')
return Promise.resolve() return Promise.resolve()
.then(() => this.parse(html)) .then(() => this.parse(html))
.then(tpl => this.render(tpl, ctx, opts)) .then(tpl => this.render(tpl, ctx, opts))
.catch(e => {
if (e instanceof Errors.RenderBreakError) {
return e.html
}
throw e
})
}, },
renderFile: function (filepath, ctx, opts) { renderFile: function (filepath, ctx, opts) {
opts = _.assign({}, opts) opts = _.assign({}, opts)
+2
View File
@@ -6,6 +6,8 @@
"scripts": { "scripts": {
"lint": "eslint src/ test/", "lint": "eslint src/ test/",
"test": "npm run lint && mocha --recursive", "test": "npm run lint && mocha --recursive",
"coverage": "NODE_ENV=test istanbul cover --report html ./node_modules/mocha/bin/_mocha -- -R spec --recursive",
"lcov": "NODE_ENV=test istanbul cover --report lcovonly ./node_modules/mocha/bin/_mocha -- -R spec --recursive",
"dist": "make dist", "dist": "make dist",
"preversion": "npm test", "preversion": "npm test",
"version": "npm run dist && git add -A dist", "version": "npm run dist && git add -A dist",
+45 -49
View File
@@ -1,7 +1,6 @@
const _ = require('./util/underscore.js') const _ = require('./util/underscore.js')
const lexical = require('./lexical.js') const lexical = require('./lexical.js')
const assert = require('./util/assert.js') const assert = require('./util/assert.js')
const toStr = Object.prototype.toString
var Scope = { var Scope = {
getAll: function () { getAll: function () {
@@ -12,32 +11,16 @@ var Scope = {
return ctx return ctx
}, },
get: function (str) { get: function (str) {
for (var i = this.scopes.length - 1; i >= 0; i--) {
try { try {
return this.getPropertyByPath(this.scopes[i], str) return this.getPropertyByPath(this.scopes, str)
} catch (e) { } catch (e) {
if (/undefined variable/.test(e.message)) { if (!/undefined variable/.test(e.message) || this.opts.strict_variables) {
continue
}
if (/Cannot read property/.test(e.message)) {
if (this.opts.strict_variables) {
e.message += ': ' + str
throw e
} else {
continue
}
} else {
e.message += ': ' + str
throw e throw e
} }
} }
}
if (this.opts.strict_variables) {
throw new TypeError('undefined variable: ' + str)
}
}, },
set: function (k, v) { set: function (k, v) {
this.setPropertyByPath(this.scopes[this.scopes.length - 1], k, v) setPropertyByPath(this.scopes[this.scopes.length - 1], k, v)
return this return this
}, },
push: function (ctx) { push: function (ctx) {
@@ -54,39 +37,23 @@ var Scope = {
shift: function () { shift: function () {
return this.scopes.shift() return this.scopes.shift()
}, },
setPropertyByPath: function (obj, path, val) {
if (_.isString(path)) {
var paths = path.replace(/\[/g, '.').replace(/\]/g, '').split('.')
for (var i = 0; i < paths.length; i++) {
var key = paths[i]
if (i === paths.length - 1) {
return (obj[key] = val)
}
if (undefined === obj[key]) obj[key] = {}
// case for readonly objects
obj = obj[key] || {}
}
}
},
getPropertyByPath: function (obj, path) { getPropertyByPath: function (scopes, path) {
var paths = this.propertyAccessSeq(path + '') var paths = this.propertyAccessSeq(path + '')
var varName = paths.shift() if (!paths.length) {
if (!obj.hasOwnProperty(varName)) { throw new TypeError('undefined variable: ' + path)
throw new TypeError('undefined variable')
} }
var variable = obj[varName] var key = paths.shift()
var lastName = paths.pop() var value = getValueFromScopes(key, scopes)
paths.forEach(p => (variable = variable[p])) return paths.reduce(
if (undefined !== lastName) { (value, key) => {
if (lastName === 'size' && if (_.isNil(value)) {
(toStr.call(variable) === '[object Array]' || throw new TypeError('undefined variable: ' + key)
toStr.call(variable) === '[object String]')) {
return variable.length
} }
variable = variable[lastName] return getValueFromParent(key, value)
} },
return variable value
)
}, },
/* /*
@@ -144,6 +111,35 @@ var Scope = {
} }
} }
function setPropertyByPath (obj, path, val) {
var paths = (path + '').replace(/\[/g, '.').replace(/\]/g, '').split('.')
for (var i = 0; i < paths.length; i++) {
var key = paths[i]
if (i === paths.length - 1) {
return (obj[key] = val)
}
if (undefined === obj[key]) obj[key] = {}
// case for readonly objects
obj = obj[key] || {}
}
}
function getValueFromParent (key, value) {
return (key === 'size' && (_.isArray(value) || _.isString(value)))
? value.length
: value[key]
}
function getValueFromScopes (key, scopes) {
for (var i = scopes.length - 1; i > -1; i--) {
var scope = scopes[i]
if (scope.hasOwnProperty(key)) {
return scope[key]
}
}
throw new TypeError('undefined variable: ' + key)
}
function matchRightBracket (str, begin) { function matchRightBracket (str, begin) {
var stack = 1 // count of '[' - count of ']' var stack = 1 // count of '[' - count of ']'
for (var i = begin; i < str.length; i++) { for (var i = begin; i < str.length; i++) {
+3 -9
View File
@@ -1,5 +1,4 @@
const lexical = require('./lexical.js') const lexical = require('./lexical.js')
const _ = require('./util/underscore.js')
const Promise = require('any-promise') const Promise = require('any-promise')
const Syntax = require('./syntax.js') const Syntax = require('./syntax.js')
const assert = require('./util/assert.js') const assert = require('./util/assert.js')
@@ -28,14 +27,9 @@ module.exports = function () {
} }
return Promise.resolve() return Promise.resolve()
.then(() => typeof impl.render === 'function' .then(() => typeof impl.render === 'function'
? impl.render(scope, obj) : '') ? impl.render(scope, obj)
.catch(function (e) { : ''
if (_.isError(e)) { )
throw e
}
var msg = `Please reject with an Error in ${impl.render}, got ${e}`
throw new Error(msg)
})
}, },
parse: function (token, tokens) { parse: function (token, tokens) {
this.type = 'tag' this.type = 'tag'
-3
View File
@@ -2,9 +2,6 @@ const AssertionError = require('./error.js').AssertionError
function assert (predicate, message) { function assert (predicate, message) {
if (!predicate) { if (!predicate) {
if (message instanceof Error) {
throw message
}
message = message || `expect ${predicate} to be true` message = message || `expect ${predicate} to be true`
throw new AssertionError(message) throw new AssertionError(message)
} }
+13 -1
View File
@@ -1,3 +1,5 @@
const toStr = Object.prototype.toString
/* /*
* Checks if value is classified as a String primitive or object. * Checks if value is classified as a String primitive or object.
* @param {any} value The value to check. * @param {any} value The value to check.
@@ -7,6 +9,15 @@ function isString (value) {
return value instanceof String || typeof value === 'string' return value instanceof String || typeof value === 'string'
} }
function isNil (value) {
return value === null || value === undefined
}
function isArray (value) {
// be compatible with IE 8
return toStr.call(value) === '[object Array]'
}
function isError (value) { function isError (value) {
var signature = Object.prototype.toString.call(value) var signature = Object.prototype.toString.call(value)
// [object XXXError] // [object XXXError]
@@ -53,7 +64,6 @@ function assign (object) {
} }
function _assignBinary (dst, src) { function _assignBinary (dst, src) {
if (!dst) return dst
forOwn(src, function (v, k) { forOwn(src, function (v, k) {
dst[k] = v dst[k] = v
}) })
@@ -115,6 +125,8 @@ function range (start, stop, step) {
exports.isString = isString exports.isString = isString
exports.isObject = isObject exports.isObject = isObject
exports.isArray = isArray
exports.isNil = isNil
exports.isError = isError exports.isError = isError
exports.range = range exports.range = range
+57 -63
View File
@@ -1,95 +1,89 @@
const Liquid = require('..'); const Liquid = require('..')
const Promise = require('any-promise'); const Promise = require('any-promise')
const lexical = Liquid.lexical; const lexical = Liquid.lexical
const assert = require('../src/util/assert.js'); const assert = require('../src/util/assert.js')
const re = new RegExp(`^(${lexical.identifier.source})\\s+in\\s+` + const re = new RegExp(`^(${lexical.identifier.source})\\s+in\\s+` +
`(${lexical.value.source})` + `(${lexical.value.source})` +
`(?:\\s+${lexical.hash.source})*$`); `(?:\\s+${lexical.hash.source})*$`)
module.exports = function(liquid) { module.exports = function (liquid) {
liquid.registerTag('tablerow', { liquid.registerTag('tablerow', {
parse: function(tagToken, remainTokens) { parse: function (tagToken, remainTokens) {
var match = re.exec(tagToken.args); var match = re.exec(tagToken.args)
assert(match, `illegal tag: ${tagToken.raw}`); assert(match, `illegal tag: ${tagToken.raw}`)
this.variable = match[1];
this.collection = match[2];
this.templates = []; this.variable = match[1]
this.collection = match[2]
this.templates = []
var p, stream = liquid.parser.parseStream(remainTokens) var p
.on('start', x => p = this.templates) var stream = liquid.parser.parseStream(remainTokens)
.on('start', () => (p = this.templates))
.on('tag:endtablerow', token => stream.stop()) .on('tag:endtablerow', token => stream.stop())
.on('template', tpl => p.push(tpl)) .on('template', tpl => p.push(tpl))
.on('end', x => { .on('end', () => {
throw new Error(`tag ${tagToken.raw} not closed`); throw new Error(`tag ${tagToken.raw} not closed`)
}); })
stream.start(); stream.start()
}, },
render: function(scope, hash) { render: function (scope, hash) {
var collection = Liquid.evalExp(this.collection, scope) || []; var collection = Liquid.evalExp(this.collection, scope) || []
var html = '<table>'; var html = '<table>'
var offset = hash.offset || 0; var offset = hash.offset || 0
var limit = (hash.limit === undefined) ? collection.length : hash.limit; var limit = (hash.limit === undefined) ? collection.length : hash.limit
var cols = hash.cols, row, col; var cols = hash.cols
if (!cols) throw new Error(`illegal cols: ${cols}`); var row
var col
if (!cols) throw new Error(`illegal cols: ${cols}`)
// build array of arguments to pass to sequential promises... // build array of arguments to pass to sequential promises...
collection = collection.slice(offset, offset + limit); collection = collection.slice(offset, offset + limit)
var contexts = []; var contexts = []
collection.some((item, i) => { collection.some((item, i) => {
var ctx = {}; var ctx = {}
ctx[this.variable] = item; ctx[this.variable] = item
// We are just putting together an array of the arguments we will be passing to our sequential promises contexts.push(ctx)
contexts.push(ctx); })
});
// This executes an array of promises sequentially for every argument in the contexts array - http://webcache.googleusercontent.com/search?q=cache:rNbMUn9TPtkJ:joost.vunderink.net/blog/2014/12/15/processing-an-array-of-promises-sequentially-in-node-js/+&cd=5&hl=en&ct=clnk&gl=us var lastPromise = contexts.reduce((promise, context, currentIndex) => promise
// It's fundamentally equivalent to the following... .then((partial) => {
// emptyPromise.then(renderTemplates(args0).then(renderTemplates(args1).then(renderTemplates(args2)... row = Math.floor(currentIndex / cols) + 1
var lastPromise = contexts.reduce((promise, context, currentIndex) => { col = (currentIndex % cols) + 1
return promise.then((partial) => { if (col === 1) {
row = Math.floor(currentIndex / cols) + 1; if (row !== 1) {
col = (currentIndex % cols) + 1; html += '</tr>'
if(col === 1) {
if(row !== 1){
html += '</tr>';
} }
html += `<tr class="row${row}">`; html += `<tr class="row${row}">`
} }
//ctx[this.variable] = context; // ctx[this.variable] = context;
html += `<td class="col${col}">`
return html += `<td class="col${col}">`; return html
}) })
.then((partial) => { .then((partial) => {
scope.push(context); scope.push(context)
return liquid.renderer.renderTemplates(this.templates, scope) return liquid.renderer.renderTemplates(this.templates, scope)
}) })
.then((partial) => { .then((partial) => {
scope.pop(context); scope.pop(context)
html += partial; html += partial
return html += '</td>'; html += '</td>'
}); return html
}, Promise.resolve('')); // start the reduce chain with a resolved Promise. After first run, the "promise" argument }), Promise.resolve(''))
// in our reduce callback will be the returned promise from our "then" above. In this
// case, the promise returned from liquid.renderer.renderTemplates.
return lastPromise return lastPromise
.then(() => { .then(() => {
if(row > 0) { if (row > 0) {
html += '</tr>'; html += '</tr>'
} }
html += '</table>'; html += '</table>'
return html; return html
}) })
.catch((error) => {
throw error;
});
} }
}); })
}; }
+6
View File
@@ -34,6 +34,12 @@ describe('liquid', function () {
afterEach(function () { afterEach(function () {
mock.restore() mock.restore()
}) })
describe('Liquid', function () {
it('should ignore invalid root option', function () {
var liquid = Liquid({ root: /regex/ })
expect(liquid.options.root).to.deep.equal([])
})
})
describe('{{output}}', function () { describe('{{output}}', function () {
it('should output object', function () { it('should output object', function () {
return expect(engine.parseAndRender('{{obj}}', ctx)).to.eventually.equal('{"foo":"bar"}') return expect(engine.parseAndRender('{{obj}}', ctx)).to.eventually.equal('{"foo":"bar"}')
+20 -3
View File
@@ -98,7 +98,13 @@ describe('scope', function () {
expect(scope.get('bar[foo]')).to.equal('coo') expect(scope.get('bar[foo]')).to.equal('coo')
}) })
it('should support nested case', function () { it('should return undefined when not exist', function () {
expect(scope.get('foo.foo.foo')).to.be.undefined
})
})
describe('#set', function () {
it('should set nested value', function () {
scope.set('posts', { scope.set('posts', {
'first': { 'first': {
'name': 'A Nice Day' 'name': 'A Nice Day'
@@ -109,8 +115,12 @@ describe('scope', function () {
}) })
expect(scope.get('posts[category.diary[0]].name'), 'A Nice Day') expect(scope.get('posts[category.diary[0]].name'), 'A Nice Day')
}) })
})
it('should create parents if needed', function () {
scope.set('foo.bar.coo', 'COO')
expect(scope.get('foo.bar.coo'), 'COO')
})
})
describe('strict_variables', function () { describe('strict_variables', function () {
var scope var scope
beforeEach(function () { beforeEach(function () {
@@ -118,12 +128,19 @@ describe('scope', function () {
strict_variables: true strict_variables: true
}) })
}) })
it('should throw undefined in strict mode', function () { it('should throw when variable not defined', function () {
function fn () { function fn () {
scope.get('notdefined') scope.get('notdefined')
} }
expect(fn).to.throw(/undefined variable: notdefined/) expect(fn).to.throw(/undefined variable: notdefined/)
}) })
it('should throw when deep variable not exist', function () {
scope.set('foo', 'bar')
function fn () {
scope.get('foo.bar.not.defined')
}
expect(fn).to.throw(/undefined variable: not/)
})
it('should find variable in parent scope', function () { it('should find variable in parent scope', function () {
scope.set('foo', 'foo') scope.set('foo', 'foo')
scope.push({ scope.push({
+22 -4
View File
@@ -7,6 +7,9 @@ describe('tags/for', function () {
var liquid, ctx var liquid, ctx
before(function () { before(function () {
liquid = Liquid() liquid = Liquid()
liquid.registerTag('throwingTag', {
render: function () { throw new Error('intended render error') }
})
ctx = { ctx = {
one: 1, one: 1,
// eslint-disable-next-line // eslint-disable-next-line
@@ -30,12 +33,20 @@ describe('tags/for', function () {
.to.eventually.equal('foo-coo-') .to.eventually.equal('foo-coo-')
}) })
it('should throw when for not closed', function () { describe('illegal', function () {
it('should reject when for not closed', function () {
var src = '{%for c in alpha%}{{c}}' var src = '{%for c in alpha%}{{c}}'
return expect(liquid.parseAndRender(src, ctx)) return expect(liquid.parseAndRender(src, ctx))
.to.be.rejectedWith(/tag .* not closed/) .to.be.rejectedWith(/tag .* not closed/)
}) })
it('should reject when inner templates rejected', function () {
var src = '{%for c in alpha%}{%throwingTag%}{%endfor%}'
return expect(liquid.parseAndRender(src, ctx))
.to.be.rejectedWith(/intended render error/)
})
})
describe('else', function () { describe('else', function () {
it('should goto else for empty array', function () { it('should goto else for empty array', function () {
var src = '{%for c in emptyArray%}a{%else%}b{%endfor%}' var src = '{%for c in emptyArray%}a{%else%}b{%endfor%}'
@@ -43,6 +54,12 @@ describe('tags/for', function () {
.to.eventually.equal('b') .to.eventually.equal('b')
}) })
it('should treat non-empty string as one single element', function () {
var src = '{%for c in "abc"%}x{{c}}{%else%}y{%endfor%}'
return expect(liquid.parseAndRender(src, ctx))
.to.eventually.equal('xabc')
})
it('should goto else for empty string', function () { it('should goto else for empty string', function () {
var src = '{%for c in ""%}a{%else%}b{%endfor%}' var src = '{%for c in ""%}a{%else%}b{%endfor%}'
return expect(liquid.parseAndRender(src, ctx)) return expect(liquid.parseAndRender(src, ctx))
@@ -95,13 +112,11 @@ describe('tags/for', function () {
'{% if i == 4 %}{% break %}{% endif %}' + '{% if i == 4 %}{% break %}{% endif %}' +
'{{ i }}' + '{{ i }}' +
'{% endfor %}' '{% endfor %}'
// return liquid.parseAndRender(src, ctx).catch(e => {
// console.log(e.stack);
// });
return expect(liquid.parseAndRender(src, ctx)) return expect(liquid.parseAndRender(src, ctx))
.to.eventually.equal('123') .to.eventually.equal('123')
}) })
describe('limit', function () {
it('should support for with limit', function () { it('should support for with limit', function () {
var src = '{% for i in (1..5) limit:2 %}{{ i }}{% endfor %}' var src = '{% for i in (1..5) limit:2 %}{{ i }}{% endfor %}'
return expect(liquid.parseAndRender(src, ctx)) return expect(liquid.parseAndRender(src, ctx))
@@ -112,7 +127,9 @@ describe('tags/for', function () {
return expect(liquid.parseAndRender(src, ctx)) return expect(liquid.parseAndRender(src, ctx))
.to.eventually.equal('67') .to.eventually.equal('67')
}) })
})
describe('reverse', function () {
it('should support for reversed in the last position', function () { it('should support for reversed in the last position', function () {
var src = '{% for i in (1..5) limit:2 reversed %}{{ i }}{% endfor %}' var src = '{% for i in (1..5) limit:2 reversed %}{{ i }}{% endfor %}'
return expect(liquid.parseAndRender(src, ctx)) return expect(liquid.parseAndRender(src, ctx))
@@ -130,4 +147,5 @@ describe('tags/for', function () {
return expect(liquid.parseAndRender(src, ctx)) return expect(liquid.parseAndRender(src, ctx))
.to.eventually.equal('543') .to.eventually.equal('543')
}) })
})
}) })
+6
View File
@@ -24,6 +24,12 @@ describe('tags/tablerow', function () {
return expect(liquid.parseAndRender(src)).to.eventually.equal(dst) return expect(liquid.parseAndRender(src)).to.eventually.equal(dst)
}) })
it('should support empty array', function () {
var src = '{% tablerow i in alpha.z cols:2 %}{{ i }}{% endtablerow %}'
var dst = '<table></table>'
return expect(liquid.parseAndRender(src)).to.eventually.equal(dst)
})
it('should throw when tablerow not closed', function () { it('should throw when tablerow not closed', function () {
var src = '{% tablerow i in (1..0) cols:2 %}{{ i }}' var src = '{% tablerow i in (1..0) cols:2 %}{{ i }}'
return expect(liquid.parseAndRender(src)) return expect(liquid.parseAndRender(src))
+18
View File
@@ -0,0 +1,18 @@
const chai = require('chai')
const expect = chai.expect
const assert = require('../../src/util/assert.js')
describe('assert', function () {
it('should not throw if predicate is truthy', function () {
var fn = () => assert('foo', 'bar')
expect(fn).to.not.throw()
})
it('should not throw if predicate is truthy', function () {
var fn = () => assert('', 'bar')
expect(fn).to.throw(/bar/)
})
it('should populate default message', function () {
var fn = () => assert(false)
expect(fn).to.throw(/expect false to be true/)
})
})
+8
View File
@@ -72,6 +72,14 @@ describe('util/underscore', function () {
expect(log).to.have.been.calledWith('[foo]', 'bar') expect(log).to.have.been.calledWith('[foo]', 'bar')
}) })
}) })
describe('.range()', function () {
it('should return a range of integers', function () {
expect(_.range(3, 5)).to.deep.equal([3, 4])
})
it('should treat start as 0 if omitted', function () {
expect(_.range(3)).to.deep.equal([0, 1, 2])
})
})
describe('.assign()', function () { describe('.assign()', function () {
it('should handle null dst', function () { it('should handle null dst', function () {
expect(_.assign(null, { expect(_.assign(null, {