cover all

This commit is contained in:
harttle
2017-08-15 23:52:05 +08:00
parent 6d59f34fc8
commit 030037eedc
14 changed files with 286 additions and 246 deletions
+107 -108
View File
@@ -1,128 +1,127 @@
const strftime = require('./src/util/strftime.js');
const _ = require('./src/util/underscore.js');
const isTruthy = require('./src/syntax.js').isTruthy;
const strftime = require('./src/util/strftime.js')
const _ = require('./src/util/underscore.js')
const isTruthy = require('./src/syntax.js').isTruthy
var escapeMap = {
'&': '&',
'<': '&lt;',
'>': '&gt;',
'"': '&#34;',
"'": '&#39;',
};
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&#34;',
"'": '&#39;'
}
var unescapeMap = {
'&amp;': '&',
'&lt;': '<',
'&gt;': '>',
'&#34;': '"',
'&#39;': "'",
};
'&amp;': '&',
'&lt;': '<',
'&gt;': '>',
'&#34;': '"',
'&#39;': "'"
}
var filters = {
'abs': v => Math.abs(v),
'append': (v, arg) => v + arg,
'capitalize': str => stringify(str).charAt(0).toUpperCase() + str.slice(1),
'ceil': v => Math.ceil(v),
'date': (v, arg) => {
if (v === 'now') v = new Date();
return v instanceof Date ? strftime(v, arg) : '';
},
'default': (v, arg) => isTruthy(v) ? v : arg,
'divided_by': (v, arg) => Math.floor(v / arg),
'downcase': v => v.toLowerCase(),
'escape': escape,
'abs': v => Math.abs(v),
'append': (v, arg) => v + arg,
'capitalize': str => stringify(str).charAt(0).toUpperCase() + str.slice(1),
'ceil': v => Math.ceil(v),
'date': (v, arg) => {
if (v === 'now') v = new Date()
return v instanceof Date ? strftime(v, arg) : ''
},
'default': (v, arg) => isTruthy(v) ? v : arg,
'divided_by': (v, arg) => Math.floor(v / arg),
'downcase': v => v.toLowerCase(),
'escape': escape,
'escape_once': str => escape(unescape(str)),
'first': v => v[0],
'floor': v => Math.floor(v),
'join': (v, arg) => v.join(arg),
'last': v => v[v.length - 1],
'lstrip': v => stringify(v).replace(/^\s+/, ''),
'map': (arr, arg) => arr.map(v => v[arg]),
'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) => Number(v) + Number(arg)),
'prepend': (v, arg) => arg + v,
'remove': (v, arg) => v.split(arg).join(''),
'remove_first': (v, l) => v.replace(l, ''),
'replace': (v, pattern, replacement) =>
stringify(v).split(pattern).join(replacement),
'replace_first': (v, arg1, arg2) => stringify(v).replace(arg1, arg2),
'reverse': v => v.reverse(),
'round': (v, arg) => {
var amp = Math.pow(10, arg || 0);
return Math.round(v * amp, arg) / amp;
},
'rstrip': str => stringify(str).replace(/\s+$/, ''),
'size': v => v.length,
'slice': (v, begin, length) =>
v.substr(begin, length === undefined ? 1 : length),
'sort': (v, arg) => v.sort(arg),
'split': (v, arg) => stringify(v).split(arg),
'strip': (v) => stringify(v).trim(),
'strip_html': v => stringify(v).replace(/<\/?\s*\w+\s*\/?>/g, ''),
'strip_newlines': v => stringify(v).replace(/\n/g, ''),
'times': (v, arg) => v * arg,
'truncate': (v, l, o) => {
v = stringify(v);
o = (o === undefined) ? '...' : o;
l = l || 16;
if (v.length <= l) return v;
return v.substr(0, l - o.length) + o;
},
'truncatewords': (v, l, o) => {
if (o === undefined) o = '...';
var arr = v.split(' ');
var ret = arr.slice(0, l).join(' ');
if (arr.length > l) ret += o;
return ret;
},
'uniq': function(arr) {
var u = {};
return (arr || []).filter(val => {
if (u.hasOwnProperty(val)) {
return false;
}
u[val] = true;
return true;
});
},
'upcase': str => stringify(str).toUpperCase(),
'url_encode': encodeURIComponent
};
function escape(str) {
return stringify(str).replace(/&|<|>|"|'/g, m => escapeMap[m]);
'escape_once': str => escape(unescape(str)),
'first': v => v[0],
'floor': v => Math.floor(v),
'join': (v, arg) => v.join(arg),
'last': v => v[v.length - 1],
'lstrip': v => stringify(v).replace(/^\s+/, ''),
'map': (arr, arg) => arr.map(v => v[arg]),
'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) => Number(v) + Number(arg)),
'prepend': (v, arg) => arg + v,
'remove': (v, arg) => v.split(arg).join(''),
'remove_first': (v, l) => v.replace(l, ''),
'replace': (v, pattern, replacement) =>
stringify(v).split(pattern).join(replacement),
'replace_first': (v, arg1, arg2) => stringify(v).replace(arg1, arg2),
'reverse': v => v.reverse(),
'round': (v, arg) => {
var amp = Math.pow(10, arg || 0)
return Math.round(v * amp, arg) / amp
},
'rstrip': str => stringify(str).replace(/\s+$/, ''),
'size': v => v.length,
'slice': (v, begin, length) =>
v.substr(begin, length === undefined ? 1 : length),
'sort': (v, arg) => v.sort(arg),
'split': (v, arg) => stringify(v).split(arg),
'strip': (v) => stringify(v).trim(),
'strip_html': v => stringify(v).replace(/<\/?\s*\w+\s*\/?>/g, ''),
'strip_newlines': v => stringify(v).replace(/\n/g, ''),
'times': (v, arg) => v * arg,
'truncate': (v, l, o) => {
v = stringify(v)
o = (o === undefined) ? '...' : o
l = l || 16
if (v.length <= l) return v
return v.substr(0, l - o.length) + o
},
'truncatewords': (v, l, o) => {
if (o === undefined) o = '...'
var arr = v.split(' ')
var ret = arr.slice(0, l).join(' ')
if (arr.length > l) ret += o
return ret
},
'uniq': function (arr) {
var u = {}
return (arr || []).filter(val => {
if (u.hasOwnProperty(val)) {
return false
}
u[val] = true
return true
})
},
'upcase': str => stringify(str).toUpperCase(),
'url_encode': encodeURIComponent
}
function unescape(str) {
return stringify(str).replace(/&(amp|lt|gt|#34|#39);/g, m => unescapeMap[m]);
function escape (str) {
return stringify(str).replace(/&|<|>|"|'/g, m => escapeMap[m])
}
function getFixed(v) {
var p = (v + "").split(".");
return (p.length > 1) ? p[1].length : 0;
function unescape (str) {
return stringify(str).replace(/&(amp|lt|gt|#34|#39);/g, m => unescapeMap[m])
}
function getMaxFixed(l, r) {
return Math.max(getFixed(l), getFixed(r));
function getFixed (v) {
var p = (v + '').split('.')
return (p.length > 1) ? p[1].length : 0
}
function stringify(obj) {
obj = obj || "";
return obj + '';
function getMaxFixed (l, r) {
return Math.max(getFixed(l), getFixed(r))
}
function bindFixed(cb) {
return (l, r) => {
var f = getMaxFixed(l, r);
return cb(l, r).toFixed(f);
};
function stringify (obj) {
return obj + ''
}
function registerAll(liquid) {
return _.forOwn(filters, (func, name) => liquid.registerFilter(name, func));
function bindFixed (cb) {
return (l, r) => {
var f = getMaxFixed(l, r)
return cb(l, r).toFixed(f)
}
}
registerAll.filters = filters;
module.exports = registerAll;
function registerAll (liquid) {
return _.forOwn(filters, (func, name) => liquid.registerFilter(name, func))
}
registerAll.filters = filters
module.exports = registerAll
+1 -4
View File
@@ -43,7 +43,6 @@ var _engine = {
return this.renderer.renderTemplates(tpl, scope)
},
parseAndRender: function (html, ctx, opts) {
console.log('parse and render')
return Promise.resolve()
.then(() => this.parse(html))
.then(tpl => this.render(tpl, ctx, opts))
@@ -69,9 +68,7 @@ var _engine = {
var paths = root.map(root => path.resolve(root, filepath))
return anySeries(paths, path => statFileAsync(path).then(() => path))
.catch((e) => {
if (e.code === 'ENOENT') {
e.message = `Failed to lookup ${filepath} in: ${root}`
}
e.message = `${e.code}: Failed to lookup ${filepath} in: ${root}`
throw e
})
},
+1
View File
@@ -82,6 +82,7 @@ function parseLiteral (str) {
if (res) {
return str.slice(1, -1)
}
throw new TypeError(`cannot parse '${str}' as literal`)
}
module.exports = {
+1
View File
@@ -40,6 +40,7 @@ function evalValue (str, scope) {
if (lexical.isVariable(str)) {
return scope.get(str)
}
throw new TypeError(`cannot eval '${str}' as value`)
}
function isTruthy (val) {
+1 -5
View File
@@ -25,11 +25,7 @@ module.exports = function () {
if (typeof impl.render !== 'function') {
return Promise.resolve('')
}
return Promise.resolve()
.then(() => typeof impl.render === 'function'
? impl.render(scope, obj)
: ''
)
return Promise.resolve().then(() => impl.render(scope, obj))
},
parse: function (token, tokens) {
this.type = 'tag'
+1
View File
@@ -163,6 +163,7 @@ var formatCodes = {
},
z: function (d) {
var tz = d.getTimezoneOffset() / 60 * 100
console.log('tz', tz)
return (tz > 0 ? '-' : '+') + _number.pad(Math.abs(tz), 4)
},
'%': function () {
+35 -39
View File
@@ -1,45 +1,41 @@
const Liquid = require('..');
const assert = require('../src/util/assert.js');
const Liquid = require('..')
module.exports = function(liquid) {
liquid.registerTag('case', {
module.exports = function (liquid) {
liquid.registerTag('case', {
parse: function(tagToken, remainTokens) {
this.cond = tagToken.args;
this.cases = [];
this.elseTemplates = [];
parse: function (tagToken, remainTokens) {
this.cond = tagToken.args
this.cases = []
this.elseTemplates = []
var p = [],
stream = liquid.parser.parseStream(remainTokens)
.on('tag:when', token => {
if (!this.cases[token.args]) {
this.cases.push({
val: token.args,
templates: p = []
});
}
})
.on('tag:else', token => p = this.elseTemplates)
.on('tag:endcase', token => stream.stop())
.on('template', tpl => p.push(tpl))
.on('end', x => {
throw new Error(`tag ${tagToken.raw} not closed`);
});
var p = []
var stream = liquid.parser.parseStream(remainTokens)
.on('tag:when', token => {
this.cases.push({
val: token.args,
templates: p = []
})
})
.on('tag:else', () => (p = this.elseTemplates))
.on('tag:endcase', token => stream.stop())
.on('template', tpl => p.push(tpl))
.on('end', x => {
throw new Error(`tag ${tagToken.raw} not closed`)
})
stream.start();
},
stream.start()
},
render: function(scope, hash) {
for (var i = 0; i < this.cases.length; i++) {
var branch = this.cases[i];
var val = Liquid.evalExp(branch.val, scope);
var cond = Liquid.evalExp(this.cond, scope);
if (val === cond) {
return liquid.renderer.renderTemplates(branch.templates, scope);
}
}
return liquid.renderer.renderTemplates(this.elseTemplates, scope);
render: function (scope, hash) {
for (var i = 0; i < this.cases.length; i++) {
var branch = this.cases[i]
var val = Liquid.evalExp(branch.val, scope)
var cond = Liquid.evalExp(this.cond, scope)
if (val === cond) {
return liquid.renderer.renderTemplates(branch.templates, scope)
}
});
};
}
return liquid.renderer.renderTemplates(this.elseTemplates, scope)
}
})
}
+37 -40
View File
@@ -1,46 +1,43 @@
const Liquid = require('..');
const Liquid = require('..')
module.exports = function(liquid) {
liquid.registerTag('if', {
module.exports = function (liquid) {
liquid.registerTag('if', {
parse: function(tagToken, remainTokens) {
parse: function (tagToken, remainTokens) {
this.branches = []
this.elseTemplates = []
this.branches = [];
this.elseTemplates = [];
var p
var stream = liquid.parser.parseStream(remainTokens)
.on('start', () => this.branches.push({
cond: tagToken.args,
templates: (p = [])
}))
.on('tag:elsif', token => {
this.branches.push({
cond: token.args,
templates: p = []
})
})
.on('tag:else', () => (p = this.elseTemplates))
.on('tag:endif', token => stream.stop())
.on('template', tpl => p.push(tpl))
.on('end', x => {
throw new Error(`tag ${tagToken.raw} not closed`)
})
var p, stream = liquid.parser.parseStream(remainTokens)
.on('start', x => this.branches.push({
cond: tagToken.args,
templates: p = []
}))
.on('tag:elsif', token => {
if (!this.branches[token.args]) {
this.branches.push({
cond: token.args,
templates: p = []
});
}
})
.on('tag:else', token => p = this.elseTemplates)
.on('tag:endif', token => stream.stop())
.on('template', tpl => p.push(tpl))
.on('end', x => {
throw new Error(`tag ${tagToken.raw} not closed`);
});
stream.start()
},
stream.start();
},
render: function(scope, hash) {
for (var i = 0; i < this.branches.length; i++) {
var branch = this.branches[i];
var cond = Liquid.evalExp(branch.cond, scope);
if (Liquid.isTruthy(cond)) {
return liquid.renderer.renderTemplates(branch.templates, scope);
}
}
return liquid.renderer.renderTemplates(this.elseTemplates, scope);
render: function (scope, hash) {
for (var i = 0; i < this.branches.length; i++) {
var branch = this.branches[i]
var cond = Liquid.evalExp(branch.cond, scope)
if (Liquid.isTruthy(cond)) {
return liquid.renderer.renderTemplates(branch.templates, scope)
}
});
};
}
return liquid.renderer.renderTemplates(this.elseTemplates, scope)
}
})
}
+13 -18
View File
@@ -1,5 +1,5 @@
const Liquid = require('..')
const Promise = require('any-promise')
const mapSeries = require('../src/util/promise.js').mapSeries
const lexical = Liquid.lexical
const assert = require('../src/util/assert.js')
const re = new RegExp(`^(${lexical.identifier.source})\\s+in\\s+` +
@@ -50,10 +50,10 @@ module.exports = function (liquid) {
contexts.push(ctx)
})
var lastPromise = contexts.reduce((promise, context, currentIndex) => promise
.then((partial) => {
row = Math.floor(currentIndex / cols) + 1
col = (currentIndex % cols) + 1
return mapSeries(contexts,
(context, idx) => {
row = Math.floor(idx / cols) + 1
col = (idx % cols) + 1
if (col === 1) {
if (row !== 1) {
html += '</tr>'
@@ -61,22 +61,17 @@ module.exports = function (liquid) {
html += `<tr class="row${row}">`
}
// ctx[this.variable] = context;
html += `<td class="col${col}">`
return html
})
.then((partial) => {
scope.push(context)
return liquid.renderer.renderTemplates(this.templates, scope)
return liquid.renderer
.renderTemplates(this.templates, scope)
.then((partial) => {
scope.pop(context)
html += partial
html += '</td>'
return html
})
})
.then((partial) => {
scope.pop(context)
html += partial
html += '</td>'
return html
}), Promise.resolve(''))
return lastPromise
.then(() => {
if (row > 0) {
html += '</tr>'
+14 -4
View File
@@ -300,6 +300,9 @@ describe('filters', function () {
it('should not truncate when short enough', function () {
return test('{{ "12345" | truncate: 5 }}', '12345')
})
it('should default to 16', function () {
return test('{{ "1234567890abcdefghi" | truncate }}', '1234567890abc...')
})
})
describe('truncatewords', function () {
@@ -321,10 +324,17 @@ describe('filters', function () {
})
})
it('should support uniq', function () {
return test('{% assign my_array = "ants, bugs, bees, bugs, ants" | split: ", " %}' +
'{{ my_array | uniq | join: ", " }}',
'ants, bugs, bees')
describe('uniq', function () {
it('should uniq string list', function () {
return test(
'{% assign my_array = "ants, bugs, bees, bugs, ants" | split: ", " %}' +
'{{ my_array | uniq | join: ", " }}',
'ants, bugs, bees'
)
})
it('should uniq falsy value', function () {
return test('{{"" | uniq | join: ","}}', '')
})
})
it('should support upcase', () => test('{{ "Parker Moore" | upcase }}', 'PARKER MOORE'))
+20 -13
View File
@@ -86,21 +86,28 @@ describe('lexical', function () {
expect(lexical.isVariable('[0][12].bar[0]')).to.equal(false)
})
it('should parse boolean literal', function () {
expect(lexical.parseLiteral('true')).to.equal(true)
expect(lexical.parseLiteral('TrUE')).to.equal(true)
expect(lexical.parseLiteral('false')).to.equal(false)
})
describe('.parseLiteral()', function () {
it('should parse boolean literal', function () {
expect(lexical.parseLiteral('true')).to.equal(true)
expect(lexical.parseLiteral('TrUE')).to.equal(true)
expect(lexical.parseLiteral('false')).to.equal(false)
})
it('should parse number literal', function () {
expect(lexical.parseLiteral('2.3')).to.equal(2.3)
expect(lexical.parseLiteral('.32')).to.equal(0.32)
expect(lexical.parseLiteral('-23.')).to.equal(-23)
expect(lexical.parseLiteral('23')).to.equal(23)
})
it('should parse number literal', function () {
expect(lexical.parseLiteral('2.3')).to.equal(2.3)
expect(lexical.parseLiteral('.32')).to.equal(0.32)
expect(lexical.parseLiteral('-23.')).to.equal(-23)
expect(lexical.parseLiteral('23')).to.equal(23)
})
it('should parse string literal', function () {
expect(lexical.parseLiteral('"ab\'c"')).to.equal("ab'c")
it('should parse string literal', function () {
expect(lexical.parseLiteral('"ab\'c"')).to.equal("ab'c")
})
it('should throw if non-literal', function () {
var fn = () => lexical.parseLiteral('a')
expect(fn).to.throw("cannot parse 'a' as literal")
})
})
describe('.matchValue()', function () {
+15 -8
View File
@@ -20,15 +20,22 @@ describe('expression', function () {
})
})
it('should eval literals', function () {
expect(evalValue('2.3')).to.equal(2.3)
expect(evalValue('"foo"')).to.equal('foo')
})
describe('.evalValue()', function () {
it('should eval literals', function () {
expect(evalValue('2.3')).to.equal(2.3)
expect(evalValue('"foo"')).to.equal('foo')
})
it('should eval variables', function () {
expect(evalValue('23', scope)).to.equal(23)
expect(evalValue('one', scope)).to.equal(1)
expect(evalValue('x', scope)).to.equal('XXX')
it('should eval variables', function () {
expect(evalValue('23', scope)).to.equal(23)
expect(evalValue('one', scope)).to.equal(1)
expect(evalValue('x', scope)).to.equal('XXX')
})
it('should throw if not valid', function () {
var fn = () => evalValue('===')
expect(fn).to.throw("cannot eval '===' as value")
})
})
describe('.isTruthy()', function () {
+12 -5
View File
@@ -6,19 +6,19 @@ chai.use(require('chai-as-promised'))
describe('tags/case', function () {
var liquid = Liquid()
it('should support case 1', function () {
it('should reject if not closed', function () {
var src = '{% case "foo"%}'
return expect(liquid.parseAndRender(src))
.to.be.rejectedWith(/{% case "foo"%} not closed/)
})
it('should support case 2', function () {
it('should hit the specified case', function () {
var src = '{% case "foo"%}' +
'{% when "foo" %}foo{% when "bar"%}bar' +
'{%endcase%}'
return expect(liquid.parseAndRender(src))
.to.eventually.equal('foo')
})
it('should support case 3', function () {
it('should resolve empty string if not hit', function () {
var src = '{% case empty %}' +
'{% when "foo" %}foo{% when ""%}bar' +
'{%endcase%}'
@@ -28,14 +28,21 @@ describe('tags/case', function () {
return expect(liquid.parseAndRender(src, ctx))
.to.eventually.equal('bar')
})
it('should support case 4', function () {
it('should accept empty string as branch name', function () {
var src = '{% case false %}' +
'{% when "foo" %}foo{% when ""%}bar' +
'{%endcase%}'
return expect(liquid.parseAndRender(src))
.to.eventually.equal('')
})
it('should support case 5', function () {
it('should support boolean case', function () {
var src = '{% case false %}' +
'{% when "foo" %}foo{% when false%}bar' +
'{%endcase%}'
return expect(liquid.parseAndRender(src))
.to.eventually.equal('bar')
})
it('should support else branch', function () {
var src = '{% case "a" %}' +
'{% when "b" %}b{% when "c"%}c{%else %}d' +
'{%endcase%}'
+28 -2
View File
@@ -36,8 +36,22 @@ describe('util/strftime', function () {
it('should format %I as 0 padded hour12', function () {
expect(t(now, '%I')).to.equal('01')
})
it('should format %j as day of year', function () {
expect(t(then, '%j')).to.equal('066')
it('should format %I as 12 for 00:00', function () {
var date = new Date('2016-01-01T00:00:00')
expect(t(date, '%I')).to.equal('12')
})
describe('%j', function () {
it('should format %j as day of year', function () {
expect(t(then, '%j')).to.equal('066')
})
it('should take count of leap years', function () {
var date = new Date('2001-03-01')
expect(t(date, '%j')).to.equal('060')
})
it('should take count of leap years', function () {
var date = new Date('2000-03-01')
expect(t(date, '%j')).to.equal('061')
})
})
it('should format %k as space padded hour', function () {
expect(t(then, '%k')).to.equal(' 3')
@@ -45,6 +59,10 @@ describe('util/strftime', function () {
it('should format %l as space padded hour12', function () {
expect(t(now, '%l')).to.equal(' 1')
})
it('should format %l as 12 for 00:00', function () {
var date = new Date('2016-01-01T00:00:00')
expect(t(date, '%l')).to.equal('12')
})
it('should format %L as 0 padded millisecond', function () {
expect(t(then, '%L')).to.equal('000')
})
@@ -94,9 +112,17 @@ describe('util/strftime', function () {
it('should format %z as time zone', function () {
expect(t(now, '%z')).to.equal('+0800')
})
it('should format %z as negative time zone', function () {
var date = new Date('2016-01-04T13:15:23')
date.getTimezoneOffset = () => 480
expect(t(date, '%z')).to.equal('-0800')
})
it('should escape %% as %', function () {
expect(t(now, '%%')).to.equal('%')
})
it('should retain un-recognized formaters', function () {
expect(t(now, '%o')).to.equal('%o')
})
})
function mockUTC () {