From f70b2ee20319fbc43697a68bc49fda8d768ed7d2 Mon Sep 17 00:00:00 2001 From: harttle Date: Thu, 3 Nov 2016 00:43:23 +0800 Subject: [PATCH] refactor: use assert, fix: respect to express settings.views, close #16 --- index.js | 42 ++++++++++++++++++++++++------------------ src/filter.js | 3 ++- src/parser.js | 3 ++- src/render.js | 29 ++++++++++++++--------------- src/scope.js | 13 +++++-------- src/syntax.js | 3 ++- src/tag.js | 7 +++---- src/tokenizer.js | 5 ++--- src/util/assert.js | 13 +++++++++++++ src/util/error.js | 12 +++++++++++- tags/assign.js | 11 ++++++----- tags/capture.js | 9 +++++---- tags/case.js | 3 ++- tags/cycle.js | 21 ++++++++++----------- tags/decrement.js | 3 ++- tags/for.js | 3 ++- tags/if.js | 2 +- tags/include.js | 14 ++++++++------ tags/increment.js | 3 ++- tags/layout.js | 9 +++++---- tags/raw.js | 2 +- tags/tablerow.js | 11 ++++++----- tags/unless.js | 2 +- test/express.js | 5 +++++ test/util/error.js | 2 +- 25 files changed, 135 insertions(+), 95 deletions(-) create mode 100644 src/util/assert.js diff --git a/index.js b/index.js index 49f5faf9a..0a69d53a1 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,6 @@ const Scope = require('./src/scope'); const _ = require('./src/util/underscore.js'); +const assert = require('./src/util/assert.js'); const tokenizer = require('./src/tokenizer.js'); const statFileAsync = require('./src/util/fs.js').statFileAsync; const readFileAsync = require('./src/util/fs.js').readFileAsync; @@ -40,12 +41,11 @@ var _engine = { opts = opts || {}; opts.strict_variables = opts.strict_variables || false; opts.strict_filters = opts.strict_filters || false; - - this.renderer.resetRegisters(); + this.renderer.initRegister(opts); var scope = Scope.factory(ctx, { strict: opts.strict_variables, }); - return this.renderer.renderTemplates(tpl, scope, opts); + return this.renderer.renderTemplates(tpl, scope); }, parseAndRender: function(html, ctx, opts) { return Promise.resolve() @@ -59,15 +59,11 @@ var _engine = { }); }, renderFile: function(filepath, ctx, opts) { - return this.getTemplate(filepath) - .then((templates) => { - return this.render(templates, ctx, opts); - }) - .catch((e) => { + opts = opts || {}; + return this.getTemplate(filepath, opts.root) + .then(templates => this.render(templates, ctx, opts)) + .catch(e => { e.file = filepath; - if (e.code === 'ENOENT') { - e.message = `Failed to lookup ${filepath} in: ${this.options.root}`; - } throw e; }); }, @@ -81,16 +77,23 @@ var _engine = { registerTag: function(name, tag) { return this.tag.register(name, tag); }, - lookup: function(filepath) { - var paths = this.options.root.map(root => pathResolve(root, filepath)); - return anySeries(paths, path => statFileAsync(path).then(() => path)); + lookup: function(filepath, root) { + root = this.options.root.concat(root || []); + var paths = root.map(root => pathResolve(root, filepath)); + return anySeries(paths, path => statFileAsync(path).then(() => path)) + .catch((e) => { + if (e.code === 'ENOENT') { + e.message = `Failed to lookup ${filepath} in: ${root}`; + } + throw e; + }); }, - getTemplate: function(filepath) { + getTemplate: function(filepath, root) { if (!filepath.match(/\.\w+$/)) { filepath += this.options.extname; } return this - .lookup(filepath) + .lookup(filepath, root) .then(filepath => { if (this.options.cache) { var tpl = this.cache[filepath]; @@ -107,8 +110,11 @@ var _engine = { }, express: function(renderOption) { renderOption = renderOption || {}; - return (filePath, options, callback) => { - this.renderFile(filePath, options, renderOption) + var self = this; + return function(filePath, options, callback) { + assert(_.isArray(this.root), 'illegal views root, are you using express.js?'); + renderOption.root = this.root; + self.renderFile(filePath, options, renderOption) .then(html => callback(null, html)) .catch(e => callback(e)); }; diff --git a/src/filter.js b/src/filter.js index cd5fb7752..d75848cb0 100644 --- a/src/filter.js +++ b/src/filter.js @@ -1,5 +1,6 @@ const lexical = require('./lexical.js'); const Syntax = require('./syntax.js'); +const assert = require('./util/assert.js'); var valueRE = new RegExp(`${lexical.value.source}`, 'g'); @@ -14,7 +15,7 @@ module.exports = function() { }, parse: function(str) { var match = lexical.filterLine.exec(str); - if (!match) throw new Error('illegal filter: ' + str); + assert(match, 'illegal filter: ' + str); var name = match[1], argList = match[2] || '', filter = filters[name]; if (typeof filter !== 'function'){ diff --git a/src/parser.js b/src/parser.js index 7ffeabf70..016ef51d3 100644 --- a/src/parser.js +++ b/src/parser.js @@ -1,5 +1,6 @@ const lexical = require('./lexical.js'); const ParseError = require('./util/error.js').ParseError; +const assert = require('./util/assert.js'); module.exports = function(Tag, Filter) { @@ -71,7 +72,7 @@ module.exports = function(Tag, Filter) { function parseOutput(str) { var match = lexical.matchValue(str); - if(!match) throw new Error(`illegal output string: ${str}`); + assert(match, `illegal output string: ${str}`); var initial = match[0]; str = str.substr(match.index + match[0].length); diff --git a/src/render.js b/src/render.js index e65449164..5687a562e 100644 --- a/src/render.js +++ b/src/render.js @@ -2,13 +2,12 @@ const Syntax = require('./syntax.js'); const Promise = require('any-promise'); const mapSeries = require('./util/promise.js').mapSeries; const RenderBreak = require('./util/error.js').RenderBreak; +const assert = require('./util/assert.js'); var render = { - renderTemplates: function(templates, scope, opts) { - if (!scope) throw new Error('unable to evalTemplates: scope undefined'); - opts = opts || {}; - opts.strict_filters = opts.strict_filters || false; + renderTemplates: function(templates, scope) { + assert(scope, 'unable to evalTemplates: scope undefined'); var html = ''; return mapSeries(templates, (tpl) => { @@ -24,10 +23,10 @@ var render = { function renderTemplate(template){ if (template.type === 'tag') { - return this.renderTag(template, scope, this.register) + return this.renderTag(template, scope) .then(partial => partial === undefined ? '' : partial); } else if (template.type === 'output') { - return Promise.resolve(this.evalOutput(template, scope, opts)) + return Promise.resolve(this.evalOutput(template, scope)) .then(partial => partial === undefined ? '' : stringify(partial)); } else { // template.type === 'html' return Promise.resolve(template.value); @@ -35,25 +34,25 @@ var render = { } }, - renderTag: function(template, scope, register) { + renderTag: function(template, scope) { if (template.name === 'continue') { return Promise.reject(new RenderBreak('continue')); } if (template.name === 'break') { return Promise.reject(new RenderBreak('break')); } - return template.render(scope, register); + return template.render(scope, this.register); }, - evalOutput: function(template, scope, opts) { - if (!scope) throw new Error('unable to evalOutput: scope undefined'); + evalOutput: function(template, scope) { + assert(scope, 'unable to evalOutput: scope undefined'); var val = Syntax.evalExp(template.initial, scope); template.filters.some(filter => { if (filter.error) { - if (opts.strict_filters) { + if (this.register.strict_filters) { throw filter.error; - } else { // render as null - val = ''; + } else { + val = '' return true; } } @@ -62,8 +61,8 @@ var render = { return val; }, - resetRegisters: function() { - return this.register = {}; + initRegister: function(opts) { + return this.register = opts; } }; diff --git a/src/scope.js b/src/scope.js index 798805c04..4741cf5db 100644 --- a/src/scope.js +++ b/src/scope.js @@ -1,5 +1,6 @@ const _ = require('./util/underscore.js'); const lexical = require('./lexical.js'); +const assert = require('./util/assert.js'); var Scope = { safeGet: function(str) { @@ -35,14 +36,14 @@ var Scope = { return this; }, push: function(ctx) { - if (!ctx) throw new Error(`trying to push ${ctx} into scopes`); + assert(ctx, `trying to push ${ctx} into scopes`); return this.scopes.push(ctx); }, pop: function() { return this.scopes.pop(); }, unshift: function(ctx) { - if (!ctx) throw new Error(`trying to push ${ctx} into scopes`); + assert(ctx, `trying to push ${ctx} into scopes`); return this.scopes.unshift(ctx); }, shift: function() { @@ -92,9 +93,7 @@ var Scope = { // foo[bar.coo] if (delemiter !== "'" && delemiter !== '"') { var j = matchRightBracket(str, i + 1); - if (j === -1) { - throw new Error(`unbalanced []: ${str}`); - } + assert(j !== -1, `unbalanced []: ${str}`); name = str.slice(i + 1, j); // foo[1] if(lexical.isInteger(name)){ @@ -110,9 +109,7 @@ var Scope = { // foo["bar"] else { var j = str.indexOf(delemiter, i + 2); - if (j === -1) { - throw new Error(`unbalanced ${delemiter}: ${str}`); - } + assert(j !== -1, `unbalanced ${delemiter}: ${str}`); name = str.slice(i + 2, j); seq.push(name); name = ''; diff --git a/src/syntax.js b/src/syntax.js index 42637000d..90d209c72 100644 --- a/src/syntax.js +++ b/src/syntax.js @@ -1,8 +1,9 @@ const operators = require('./operators.js'); const lexical = require('./lexical.js'); +const assert = require('../src/util/assert.js'); function evalExp(exp, scope) { - if (!scope) throw new Error('unable to evalExp: scope undefined'); + assert(scope, 'unable to evalExp: scope undefined'); var operatorREs = lexical.operators, match; for (var i = 0; i < operatorREs.length; i++) { diff --git a/src/tag.js b/src/tag.js index 1388e21a9..2f47d0b66 100644 --- a/src/tag.js +++ b/src/tag.js @@ -1,6 +1,7 @@ const lexical = require('./lexical.js'); const Promise = require('any-promise'); const Syntax = require('./syntax.js'); +const assert = require('./util/assert.js'); function hash(markup, scope) { var obj = {}, match; @@ -18,10 +19,8 @@ module.exports = function() { var _tagInstance = { render: function(scope, register) { - var reg = register[this.name]; - if(!reg) reg = register[this.name] = {}; var obj = hash(this.token.args, scope); - return this.tagImpl.render && this.tagImpl.render(scope, obj, reg) || Promise.resolve(''); + return this.tagImpl.render && this.tagImpl.render(scope, obj, register) || Promise.resolve(''); }, parse: function(token, tokens){ this.type = 'tag'; @@ -29,7 +28,7 @@ module.exports = function() { this.name = token.name; var tagImpl = tagImpls[this.name]; - if (!tagImpl) throw new Error(`tag ${this.name} not found`); + assert(tagImpl, `tag ${this.name} not found`); this.tagImpl = Object.create(tagImpl); if(this.tagImpl.parse){ this.tagImpl.parse(token, tokens); diff --git a/src/tokenizer.js b/src/tokenizer.js index 48400f5d8..61c783660 100644 --- a/src/tokenizer.js +++ b/src/tokenizer.js @@ -1,12 +1,11 @@ const lexical = require('./lexical.js'); const TokenizationError = require('./util/error.js').TokenizationError; const _ = require('./util/underscore.js'); +const assert = require('../src/util/assert.js'); function parse(html) { var tokens = []; - if (!_.isString(html)) { - throw new TokenizationError('illegal input type'); - } + assert(_.isString(html), new TokenizationError('illegal input type')); var syntax = /({%(.*?)%})|({{(.*?)}})/g; var result, htmlFragment, token; diff --git a/src/util/assert.js b/src/util/assert.js new file mode 100644 index 000000000..a27b89142 --- /dev/null +++ b/src/util/assert.js @@ -0,0 +1,13 @@ +const AssertionError = require('./error.js').AssertionError; + +function assert(predicate, message) { + if (!predicate) { + if (message instanceof Error) { + throw message; + } + var message = message || `expect ${predicate} to be true`; + throw new AssertionError(message); + } +} + +module.exports = assert; diff --git a/src/util/error.js b/src/util/error.js index 0819bf4a3..1ed8ebe26 100644 --- a/src/util/error.js +++ b/src/util/error.js @@ -35,6 +35,16 @@ function RenderBreak(message){ RenderBreak.prototype = Object.create(Error.prototype); RenderBreak.prototype.constructor = RenderBreak; +function AssertionError(message){ + if(Error.captureStackTrace){ + Error.captureStackTrace(this, this.constructor); + } + this.name = this.constructor.name; + this.message = message; +} +AssertionError.prototype = Object.create(Error.prototype); +AssertionError.prototype.constructor = AssertionError; + module.exports = { - TokenizationError, ParseError, RenderBreak + TokenizationError, ParseError, RenderBreak, AssertionError }; diff --git a/tags/assign.js b/tags/assign.js index d0d67ebb6..a5e8b9557 100644 --- a/tags/assign.js +++ b/tags/assign.js @@ -1,14 +1,15 @@ -var Liquid = require('..'); -var lexical = Liquid.lexical; -var Promise = require('any-promise'); -var re = new RegExp(`(${lexical.identifier.source})\\s*=(.*)`); +const Liquid = require('..'); +const lexical = Liquid.lexical; +const Promise = require('any-promise'); +const re = new RegExp(`(${lexical.identifier.source})\\s*=(.*)`); +const assert = require('../src/util/assert.js'); module.exports = function(liquid) { liquid.registerTag('assign', { parse: function(token){ var match = token.args.match(re); - if(!match) throw new Error(`illegal token ${token.raw}`); + assert(match, `illegal token ${token.raw}`); this.key = match[1]; this.value = match[2]; }, diff --git a/tags/capture.js b/tags/capture.js index 6a315887f..da3fd3f2b 100644 --- a/tags/capture.js +++ b/tags/capture.js @@ -1,13 +1,14 @@ -var Liquid = require('..'); -var lexical = Liquid.lexical; -var re = new RegExp(`(${lexical.identifier.source})`); +const Liquid = require('..'); +const lexical = Liquid.lexical; +const re = new RegExp(`(${lexical.identifier.source})`); +const assert = require('../src/util/assert.js'); module.exports = function(liquid) { liquid.registerTag('capture', { parse: function(tagToken, remainTokens) { var match = tagToken.args.match(re); - if (!match) throw new Error(`${tagToken.args} not valid identifier`); + assert(match, `${tagToken.args} not valid identifier`); this.variable = match[1]; this.templates = []; diff --git a/tags/case.js b/tags/case.js index cea749542..76d5cff58 100644 --- a/tags/case.js +++ b/tags/case.js @@ -1,4 +1,5 @@ -var Liquid = require('..'); +const Liquid = require('..'); +const assert = require('../src/util/assert.js'); module.exports = function(liquid) { liquid.registerTag('case', { diff --git a/tags/cycle.js b/tags/cycle.js index d1ef96cf7..02b2989dd 100644 --- a/tags/cycle.js +++ b/tags/cycle.js @@ -1,15 +1,16 @@ -var Liquid = require('..'); -var Promise = require('any-promise'); -var lexical = Liquid.lexical; -var groupRE = new RegExp(`^(?:(${lexical.value.source})\\s*:\\s*)?(.*)$`); -var candidatesRE = new RegExp(lexical.value.source, 'g'); +const Liquid = require('..'); +const Promise = require('any-promise'); +const lexical = Liquid.lexical; +const groupRE = new RegExp(`^(?:(${lexical.value.source})\\s*:\\s*)?(.*)$`); +const candidatesRE = new RegExp(lexical.value.source, 'g'); +const assert = require('../src/util/assert.js'); module.exports = function(liquid) { liquid.registerTag('cycle', { parse: function(tagToken, remainTokens) { var match = groupRE.exec(tagToken.args); - if(!match) throw new Error(`illegal tag: ${tagToken.raw}`); + assert(match, `illegal tag: ${tagToken.raw}`); this.group = match[1] || ''; var candidates = match[2]; @@ -20,14 +21,12 @@ module.exports = function(liquid) { this.candidates.push(match[0]); } - if (!this.candidates.length){ - throw new Error(`empty candidates: ${tagToken.raw}`); - } + assert(this.candidates.length, `empty candidates: ${tagToken.raw}`); }, render: function(scope, hash, register) { - var fingerprint = Liquid.evalValue(this.group, scope) + ':' + - this.candidates.join(','); + var group = Liquid.evalValue(this.group, scope); + var fingerprint = `cycle:${group}:` + this.candidates.join(','); var idx = register[fingerprint]; if(idx === undefined){ diff --git a/tags/decrement.js b/tags/decrement.js index 327002b5a..1e5274406 100644 --- a/tags/decrement.js +++ b/tags/decrement.js @@ -1,12 +1,13 @@ const Liquid = require('..'); const lexical = Liquid.lexical; +const assert = require('../src/util/assert.js'); module.exports = function(liquid) { liquid.registerTag('decrement', { parse: function(token) { var match = token.args.match(lexical.identifier); - if (!match) throw new Error(`illegal identifier ${token.args}`); + assert(match, `illegal identifier ${token.args}`); this.variable = match[0]; }, render: function(scope, hash) { diff --git a/tags/for.js b/tags/for.js index e31660d5c..d5fd8c136 100644 --- a/tags/for.js +++ b/tags/for.js @@ -3,6 +3,7 @@ const Promise = require('any-promise'); const lexical = Liquid.lexical; const mapSeries = require('../src/util/promise.js').mapSeries; const RenderBreak = Liquid.Types.RenderBreak; +const assert = require('../src/util/assert.js'); const re = new RegExp(`^(${lexical.identifier.source})\\s+in\\s+` + `(${lexical.value.source})` + `(?:\\s+${lexical.hash.source})*` + @@ -13,7 +14,7 @@ module.exports = function(liquid) { parse: function(tagToken, remainTokens) { var match = re.exec(tagToken.args); - if (!match) throw new Error(`illegal tag: ${tagToken.raw}`); + assert(match, `illegal tag: ${tagToken.raw}`); this.variable = match[1]; this.collection = match[2]; this.reversed = !!match[3]; diff --git a/tags/if.js b/tags/if.js index b83cab6dc..6c65e9904 100644 --- a/tags/if.js +++ b/tags/if.js @@ -1,4 +1,4 @@ -var Liquid = require('..'); +const Liquid = require('..'); module.exports = function(liquid) { liquid.registerTag('if', { diff --git a/tags/include.js b/tags/include.js index b3b348bec..b334c435e 100644 --- a/tags/include.js +++ b/tags/include.js @@ -1,13 +1,14 @@ -var Liquid = require('..'); -var lexical = Liquid.lexical; -var withRE = new RegExp(`with\\s+(${lexical.value.source})`); +const Liquid = require('..'); +const lexical = Liquid.lexical; +const withRE = new RegExp(`with\\s+(${lexical.value.source})`); +const assert = require('../src/util/assert.js'); module.exports = function(liquid) { liquid.registerTag('include', { parse: function(token){ var match = lexical.value.exec(token.args); - if(!match) throw(new Error(`illegal token ${token.raw}`)); + assert(match, `illegal token ${token.raw}`); this.value = match[0]; match = withRE.exec(token.args); @@ -15,12 +16,13 @@ module.exports = function(liquid) { this.with = match[1]; } }, - render: function(scope, hash) { + render: function(scope, hash, register) { + console.log('include', register.root); var filepath = Liquid.evalValue(this.value, scope); if(this.with){ hash[filepath] = Liquid.evalValue(this.with, scope); } - return liquid.getTemplate(filepath) + return liquid.getTemplate(filepath, register.root) .then((templates) => { scope.push(hash); return liquid.renderer.renderTemplates(templates, scope); diff --git a/tags/increment.js b/tags/increment.js index 0e253fa87..ada4aeb89 100644 --- a/tags/increment.js +++ b/tags/increment.js @@ -1,4 +1,5 @@ const Liquid = require('..'); +const assert = require('../src/util/assert.js'); const lexical = Liquid.lexical; module.exports = function(liquid) { @@ -6,7 +7,7 @@ module.exports = function(liquid) { liquid.registerTag('increment', { parse: function(token) { var match = token.args.match(lexical.identifier); - if (!match) throw (new Error(`illegal identifier ${token.args}`)); + assert(match, `illegal identifier ${token.args}`); this.variable = match[0]; }, render: function(scope, hash) { diff --git a/tags/layout.js b/tags/layout.js index 5c7cd9b84..91333763e 100644 --- a/tags/layout.js +++ b/tags/layout.js @@ -1,13 +1,14 @@ -var Liquid = require('..'); -var Promise = require('any-promise'); -var lexical = Liquid.lexical; +const Liquid = require('..'); +const Promise = require('any-promise'); +const lexical = Liquid.lexical; +const assert = require('../src/util/assert.js'); module.exports = function(liquid) { liquid.registerTag('layout', { parse: function(token, remainTokens){ var match = lexical.value.exec(token.args); - if(!match) throw new Error(`illegal token ${token.raw}`); + assert(match, `illegal token ${token.raw}`); this.layout = match[0]; this.tpls = liquid.parser.parse(remainTokens); diff --git a/tags/raw.js b/tags/raw.js index 0ac7dc5b4..854a03fe8 100644 --- a/tags/raw.js +++ b/tags/raw.js @@ -1,4 +1,4 @@ -var Promise = require('any-promise'); +const Promise = require('any-promise'); module.exports = function(liquid) { diff --git a/tags/tablerow.js b/tags/tablerow.js index 95f84afc7..bd5407eff 100644 --- a/tags/tablerow.js +++ b/tags/tablerow.js @@ -1,7 +1,8 @@ -var Liquid = require('..'); -var Promise = require('any-promise'); -var lexical = Liquid.lexical; -var re = new RegExp(`^(${lexical.identifier.source})\\s+in\\s+` + +const Liquid = require('..'); +const Promise = require('any-promise'); +const lexical = Liquid.lexical; +const assert = require('../src/util/assert.js'); +const re = new RegExp(`^(${lexical.identifier.source})\\s+in\\s+` + `(${lexical.value.source})` + `(?:\\s+${lexical.hash.source})*$`); @@ -10,7 +11,7 @@ module.exports = function(liquid) { parse: function(tagToken, remainTokens) { var match = re.exec(tagToken.args); - if (!match) throw new Error(`illegal tag: ${tagToken.raw}`); + assert(match, `illegal tag: ${tagToken.raw}`); this.variable = match[1]; this.collection = match[2]; diff --git a/tags/unless.js b/tags/unless.js index c5589c408..e865ddfb2 100644 --- a/tags/unless.js +++ b/tags/unless.js @@ -1,4 +1,4 @@ -var Liquid = require('..'); +const Liquid = require('..'); module.exports = function(liquid) { liquid.registerTag('unless', { diff --git a/test/express.js b/test/express.js index 635d9e041..19b5cd2f3 100644 --- a/test/express.js +++ b/test/express.js @@ -66,4 +66,9 @@ describe('engine#express()', function() { .expect('foo') .expect(200, done); }); + it('should respect express settings.views when lookup', function(done) { + request(app).get('/include/bar') + .expect('bar') + .expect(200, done); + }); }); diff --git a/test/util/error.js b/test/util/error.js index 5d1e5374e..a1f3aa1e2 100644 --- a/test/util/error.js +++ b/test/util/error.js @@ -37,10 +37,10 @@ describe('error', function() { "/foo.html": '\n\n\n{% raw %}\n\n' }); return test(engine.renderFile('/foo.html', {}), function(err){ + mock.restore(); expect(err.input).to.equal('{% raw %}'); expect(err.line).to.equal(4); expect(err.file).to.equal('/foo.html'); - mock.restore(); }); });