diff --git a/filters.js b/filters.js index 9c4c6fa51..e644d3624 100644 --- a/filters.js +++ b/filters.js @@ -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 = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', -}; + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' +} var unescapeMap = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - ''': "'", -}; + '&': '&', + '<': '<', + '>': '>', + '"': '"', + ''': "'" +} 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, '
'), - '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, '
'), + '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 diff --git a/index.js b/index.js index 78768a01b..6ec377f5b 100644 --- a/index.js +++ b/index.js @@ -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 }) }, diff --git a/src/lexical.js b/src/lexical.js index 5145b4d24..e3e7259c8 100644 --- a/src/lexical.js +++ b/src/lexical.js @@ -82,6 +82,7 @@ function parseLiteral (str) { if (res) { return str.slice(1, -1) } + throw new TypeError(`cannot parse '${str}' as literal`) } module.exports = { diff --git a/src/syntax.js b/src/syntax.js index 889eec01e..aa019bfbf 100644 --- a/src/syntax.js +++ b/src/syntax.js @@ -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) { diff --git a/src/tag.js b/src/tag.js index ecbf591fb..4411048a7 100644 --- a/src/tag.js +++ b/src/tag.js @@ -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' diff --git a/src/util/strftime.js b/src/util/strftime.js index 8afd3797b..9eeb0acf3 100644 --- a/src/util/strftime.js +++ b/src/util/strftime.js @@ -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 () { diff --git a/tags/case.js b/tags/case.js index 76d5cff58..ec74407cd 100644 --- a/tags/case.js +++ b/tags/case.js @@ -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) + } + }) +} diff --git a/tags/if.js b/tags/if.js index 6c65e9904..486238e56 100644 --- a/tags/if.js +++ b/tags/if.js @@ -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) + } + }) +} diff --git a/tags/tablerow.js b/tags/tablerow.js index b45dac04b..027f2106b 100644 --- a/tags/tablerow.js +++ b/tags/tablerow.js @@ -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 += '' @@ -61,22 +61,17 @@ module.exports = function (liquid) { html += `` } - // ctx[this.variable] = context; html += `` - 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 += '' + return html + }) }) - .then((partial) => { - scope.pop(context) - html += partial - html += '' - return html - }), Promise.resolve('')) - - return lastPromise .then(() => { if (row > 0) { html += '' diff --git a/test/filters.js b/test/filters.js index ac2968acc..7998ec13d 100644 --- a/test/filters.js +++ b/test/filters.js @@ -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')) diff --git a/test/lexical.js b/test/lexical.js index 949b751f3..2795bef87 100644 --- a/test/lexical.js +++ b/test/lexical.js @@ -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 () { diff --git a/test/syntax.js b/test/syntax.js index df7df233f..2efd36599 100644 --- a/test/syntax.js +++ b/test/syntax.js @@ -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 () { diff --git a/test/tags/case.js b/test/tags/case.js index 4476e3f24..e35de1290 100644 --- a/test/tags/case.js +++ b/test/tags/case.js @@ -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%}' diff --git a/test/util/strftime.js b/test/util/strftime.js index 3af0cedf4..4ebaed3bb 100644 --- a/test/util/strftime.js +++ b/test/util/strftime.js @@ -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 () {