From c3de3fbb197ab3ed07f515da8e9be50f9d1fc4e1 Mon Sep 17 00:00:00 2001 From: harttle Date: Sat, 21 Jul 2018 22:59:52 +0800 Subject: [PATCH] fix: post-increment, pre-decrement working on #76 --- filters.js | 23 +++++++++++---------- src/scope.js | 43 ++++++++++++++++++++++++--------------- src/tag.js | 21 ++++++++++--------- src/util/url.js | 3 ++- tags/assign.js | 3 ++- tags/decrement.js | 20 +++++++++++++++--- tags/increment.js | 24 ++++++++++++++++++---- tags/raw.js | 5 ++--- test/filters.js | 4 ++-- test/tags/decrement.js | 46 +++++++++++++++++++++++------------------- test/tags/increment.js | 45 +++++++++++++++++++++++------------------ test/util/error.js | 1 + 12 files changed, 146 insertions(+), 92 deletions(-) diff --git a/filters.js b/filters.js index d363160b9..3842255e1 100644 --- a/filters.js +++ b/filters.js @@ -1,15 +1,16 @@ +'use strict' const strftime = require('./src/util/strftime.js') const _ = require('./src/util/underscore.js') const isTruthy = require('./src/syntax.js').isTruthy -var escapeMap = { +let escapeMap = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' } -var unescapeMap = { +let unescapeMap = { '&': '&', '<': '<', '>': '>', @@ -17,14 +18,14 @@ var unescapeMap = { ''': "'" } -var filters = { +let 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), 'concat': (v, arg) => Array.prototype.concat.call(v, arg), 'date': (v, arg) => { - var date = v + let date = v if (v === 'now') { date = new Date() } else if (_.isString(v)) { @@ -41,7 +42,7 @@ var filters = { 'first': v => v[0], 'floor': v => Math.floor(v), 'join': (v, arg) => v.join(arg), - 'last': v => v[v.length - 1], + 'last': v => _.last(v), 'lstrip': v => stringify(v).replace(/^\s+/, ''), 'map': (arr, arg) => arr.map(v => v[arg]), 'minus': bindFixed((v, arg) => v - arg), @@ -56,7 +57,7 @@ var filters = { 'replace_first': (v, arg1, arg2) => stringify(v).replace(arg1, arg2), 'reverse': v => v.reverse(), 'round': (v, arg) => { - var amp = Math.pow(10, arg || 0) + let amp = Math.pow(10, arg || 0) return Math.round(v * amp, arg) / amp }, 'rstrip': str => stringify(str).replace(/\s+$/, ''), @@ -78,13 +79,13 @@ var filters = { }, 'truncatewords': (v, l, o) => { if (o === undefined) o = '...' - var arr = v.split(' ') - var ret = arr.slice(0, l).join(' ') + let arr = v.split(' ') + let ret = arr.slice(0, l).join(' ') if (arr.length > l) ret += o return ret }, 'uniq': function (arr) { - var u = {} + let u = {} return (arr || []).filter(val => { if (u.hasOwnProperty(val)) { return false @@ -106,7 +107,7 @@ function unescape (str) { } function getFixed (v) { - var p = (v + '').split('.') + let p = (v + '').split('.') return (p.length > 1) ? p[1].length : 0 } @@ -120,7 +121,7 @@ function stringify (obj) { function bindFixed (cb) { return (l, r) => { - var f = getMaxFixed(l, r) + let f = getMaxFixed(l, r) return cb(l, r).toFixed(f) } } diff --git a/src/scope.js b/src/scope.js index a3a03fae7..8899165e4 100644 --- a/src/scope.js +++ b/src/scope.js @@ -5,16 +5,16 @@ const assert = require('./util/assert.js') var Scope = { getAll: function () { - return this.scopes.reduce((ctx, val) => Object.assign(ctx, val), Object.create(null)) + return this.contexts.reduce((ctx, val) => Object.assign(ctx, val), Object.create(null)) }, get: function (path) { let paths = this.propertyAccessSeq(path) - let scope = this.findScopeFor(paths[0]) + let scope = this.findContextFor(paths[0]) || _.last(this.contexts) return paths.reduce((value, key) => this.readProperty(value, key), scope) }, set: function (path, v) { let paths = this.propertyAccessSeq(path) - let scope = this.findScopeFor(paths[0]) + let scope = this.findContextFor(paths[0]) || _.last(this.contexts) paths.some((key, i) => { if (!_.isObject(scope)) { return true @@ -29,28 +29,32 @@ var Scope = { scope = scope[key] }) }, + unshift: function (ctx) { + return this.contexts.unshift(ctx) + }, push: function (ctx) { - this.scopes.push(ctx) + return this.contexts.push(ctx) }, pop: function (ctx) { if (!arguments.length) { - return this.scopes.pop() + return this.contexts.pop() } - let i = this.scopes.findIndex(scope => scope === ctx) + let i = this.contexts.findIndex(scope => scope === ctx) if (i === -1) { throw new TypeError('scope not found, cannot pop') } - return this.scopes.splice(i, 1)[0] + return this.contexts.splice(i, 1)[0] }, - findScopeFor: function (key) { - let i = this.scopes.length - 1 - while (i >= 0 && !(key in this.scopes[i])) { - i-- + findContextFor: function (key, filter) { + filter = filter || (() => true) + for (let i = this.contexts.length - 1; i >= 0; i--) { + let candidate = this.contexts[i] + if (!filter(candidate)) continue + if (key in candidate) { + return candidate + } } - if (i < 0) { - i = this.scopes.length - 1 - } - return this.scopes[i] + return null }, readProperty: function (obj, key) { let val @@ -153,6 +157,13 @@ exports.factory = function (ctx, opts) { } var scope = Object.create(Scope) scope.opts = _.assign(defaultOptions, opts) - scope.scopes = [ctx || {}] + scope.contexts = [ctx || {}] return scope } + +exports.types = { + AssignScope: Object.create(null), + CaptureScope: Object.create(null), + IncrementScope: Object.create(null), + DecrementScope: Object.create(null) +} diff --git a/src/tag.js b/src/tag.js index 720636beb..d9af6a50f 100644 --- a/src/tag.js +++ b/src/tag.js @@ -1,26 +1,27 @@ +'use strict' const lexical = require('./lexical.js') const Syntax = require('./syntax.js') const assert = require('./util/assert.js') function hash (markup, scope) { - var obj = {} - var match + let obj = {} + let match lexical.hashCapture.lastIndex = 0 while ((match = lexical.hashCapture.exec(markup))) { - var k = match[1] - var v = match[2] + let k = match[1] + let v = match[2] obj[k] = Syntax.evalValue(v, scope) } return obj } module.exports = function () { - var tagImpls = {} + let tagImpls = {} - var _tagInstance = { + let _tagInstance = { render: function (scope) { - var obj = hash(this.token.args, scope) - var impl = this.tagImpl + let obj = hash(this.token.args, scope) + let impl = this.tagImpl if (typeof impl.render !== 'function') { return Promise.resolve('') } @@ -31,7 +32,7 @@ module.exports = function () { this.token = token this.name = token.name - var tagImpl = tagImpls[this.name] + let tagImpl = tagImpls[this.name] assert(tagImpl, `tag ${this.name} not found`) this.tagImpl = Object.create(tagImpl) if (this.tagImpl.parse) { @@ -45,7 +46,7 @@ module.exports = function () { } function construct (token, tokens) { - var instance = Object.create(_tagInstance) + let instance = Object.create(_tagInstance) instance.parse(token, tokens) return instance } diff --git a/src/util/url.js b/src/util/url.js index a7a6057e9..e1068adbc 100644 --- a/src/util/url.js +++ b/src/util/url.js @@ -1,6 +1,7 @@ const resolve = require('resolve-url') const splitPathRe = /^(\/?|)([\s\S]*?)((?:\.{1,2}|[^/]+?|)(\.[^./]*|))(?:[/]*)$/ const urlRe = /^(?:\w+:)?\/\/([^\s.]+\.\S{2}|localhost[:?\d]*)\S*$/ +const _ = require('./underscore') // https://github.com/jinder/path/blob/master/path.js#L567 exports.extname = function (path) { @@ -16,7 +17,7 @@ exports.resolve = function (root, path) { if (Object.prototype.toString.call(root) === '[object Array]') { root = root[0] } - if (root && root.charAt(root.length - 1) !== '/') { + if (root && _.last(root) !== '/') { root += '/' } return resolve(root, path) diff --git a/tags/assign.js b/tags/assign.js index c25119207..502490adb 100644 --- a/tags/assign.js +++ b/tags/assign.js @@ -3,6 +3,7 @@ const Liquid = require('..') const lexical = Liquid.lexical const re = new RegExp(`(${lexical.identifier.source})\\s*=(.*)`) const assert = require('../src/util/assert.js') +const types = require('../src/scope').types module.exports = function (liquid) { liquid.registerTag('assign', { @@ -13,7 +14,7 @@ module.exports = function (liquid) { this.value = match[2] }, render: function (scope) { - let ctx = Object.create(null) + let ctx = Object.create(types.AssignScope) ctx[this.key] = liquid.evalValue(this.value, scope) scope.push(ctx) return Promise.resolve('') diff --git a/tags/decrement.js b/tags/decrement.js index 080914821..8fc968135 100644 --- a/tags/decrement.js +++ b/tags/decrement.js @@ -1,6 +1,8 @@ +'use strict' const Liquid = require('..') const lexical = Liquid.lexical const assert = require('../src/util/assert.js') +const types = require('../src/scope').types module.exports = function (liquid) { liquid.registerTag('decrement', { @@ -10,9 +12,21 @@ module.exports = function (liquid) { this.variable = match[0] }, render: function (scope, hash) { - var v = scope.get(this.variable) - if (typeof v !== 'number') v = 0 - scope.set(this.variable, v - 1) + let context = scope.findContextFor( + this.variable, + ctx => { + return Object.getPrototypeOf(ctx) !== types.CaptureScope && + Object.getPrototypeOf(ctx) !== types.AssignScope + } + ) + if (!context) { + context = Object.create(types.DecrementScope) + scope.unshift(context) + } + if (typeof context[this.variable] !== 'number') { + context[this.variable] = 0 + } + return --context[this.variable] } }) } diff --git a/tags/increment.js b/tags/increment.js index ddba6fc85..f72e9057d 100644 --- a/tags/increment.js +++ b/tags/increment.js @@ -1,18 +1,34 @@ +'use strict' const Liquid = require('..') const assert = require('../src/util/assert.js') const lexical = Liquid.lexical +const types = require('../src/scope').types module.exports = function (liquid) { liquid.registerTag('increment', { parse: function (token) { - var match = token.args.match(lexical.identifier) + let match = token.args.match(lexical.identifier) assert(match, `illegal identifier ${token.args}`) this.variable = match[0] }, render: function (scope, hash) { - var v = scope.get(this.variable) - if (typeof v !== 'number') v = 0 - scope.set(this.variable, v + 1) + let context = scope.findContextFor( + this.variable, + ctx => { + return Object.getPrototypeOf(ctx) !== types.CaptureScope && + Object.getPrototypeOf(ctx) !== types.AssignScope + } + ) + if (!context) { + context = Object.create(types.IncrementScope) + scope.unshift(context) + } + if (typeof context[this.variable] !== 'number') { + context[this.variable] = 0 + } + let val = context[this.variable] + context[this.variable]++ + return val } }) } diff --git a/tags/raw.js b/tags/raw.js index aa9ae60a2..9f11d4881 100644 --- a/tags/raw.js +++ b/tags/raw.js @@ -9,14 +9,13 @@ module.exports = function (liquid) { if (token.name === 'endraw') stream.stop() else this.tokens.push(token) }) - .on('end', x => { + .on('end', () => { throw new Error(`tag ${tagToken.raw} not closed`) }) stream.start() }, render: function (scope, hash) { - var tokens = this.tokens.map(token => token.raw).join('') - return Promise.resolve(tokens) + return this.tokens.map(token => token.raw).join('') } }) } diff --git a/test/filters.js b/test/filters.js index 631b196c5..becfd33b3 100644 --- a/test/filters.js +++ b/test/filters.js @@ -141,7 +141,7 @@ describe('filters', function () { it('should support split/first', function () { var src = '{% assign my_array = "apples, oranges, peaches, plums" | split: ", " %}' + - '{{ my_array | first }}' + '{{ my_array | first }}' return test(src, 'apples') }) @@ -160,7 +160,7 @@ describe('filters', function () { it('should support split/last', function () { var src = '{% assign my_array = "zebra, octopus, giraffe, tiger" | split: ", " %}' + - '{{ my_array|last }}' + '{{ my_array|last }}' return test(src, 'tiger') }) diff --git a/test/tags/decrement.js b/test/tags/decrement.js index 5e1d44bd2..fe499831f 100644 --- a/test/tags/decrement.js +++ b/test/tags/decrement.js @@ -1,3 +1,4 @@ +'use strict' const Liquid = require('../..') const chai = require('chai') const expect = chai.expect @@ -5,34 +6,37 @@ chai.use(require('chai-as-promised')) describe('tags/decrement', function () { var liquid = Liquid() - it('should throw when variable expression illegal', function () { - var src = '{% decrement / %}{{one}}' + var src = '{% decrement / %}{{var}}' var ctx = {} return expect(liquid.parseAndRender(src, ctx)).to.be.rejectedWith(/illegal/) }) - it('should support decrement', function () { - var src = '{% decrement one %}{{one}}' - var ctx = { - one: 1 - } - return expect(liquid.parseAndRender(src, ctx)) - .to.eventually.equal('0') - }) - - it('should decrement undefined', function () { - var src = '{% decrement empty %}{{empty}}' + it('should decrement undefined variable', function () { + let src = '{% decrement var %}{% decrement var %}{% decrement var %}' return expect(liquid.parseAndRender(src)) - .to.eventually.equal('-1') + .to.eventually.equal('-1-2-3') }) - it('should support decrement multiple times', function () { - var src = '{% decrement foo %}{%decrement foo%}{{foo}}' - var ctx = { - foo: 1 - } - return expect(liquid.parseAndRender(src, ctx)) - .to.eventually.equal('-1') + it('should decrement defined variable', function () { + let src = '{% decrement var %}{% decrement var %}{% decrement var %}' + let ctx = {'var': 10} + return liquid.parseAndRender(src, ctx) + .then(x => { + expect(x).to.equal('987') + expect(ctx.var).to.equal(7) + }) + }) + + it('should be independent from assign', function () { + let src = '{% assign var=10 %}{% decrement var %}{% decrement var %}{% decrement var %}' + return expect(liquid.parseAndRender(src)) + .to.eventually.equal('-1-2-3') + }) + + it('should not shading assign', function () { + let src = '{% assign var=10 %}{% decrement var %}{% decrement var %}{% decrement var %} {{var}}' + return expect(liquid.parseAndRender(src)) + .to.eventually.equal('-1-2-3 10') }) }) diff --git a/test/tags/increment.js b/test/tags/increment.js index 03eac0a38..1c4578133 100644 --- a/test/tags/increment.js +++ b/test/tags/increment.js @@ -1,32 +1,37 @@ +'use strict' const Liquid = require('../..') const chai = require('chai') const expect = chai.expect chai.use(require('chai-as-promised')) describe('tags/increment', function () { - var liquid = Liquid() + let liquid = Liquid() - it('should support increment', function () { - var src = '{% increment one %}{{one}}' - var ctx = { - one: 1 - } - return expect(liquid.parseAndRender(src, ctx)) - .to.eventually.equal('2') - }) - - it('should increment undefined', function () { - var src = '{% increment empty %}{{empty}}' + it('should increment undefined variable', function () { + let src = '{% increment one %}{% increment one %}{% increment one %}' return expect(liquid.parseAndRender(src)) - .to.eventually.equal('1') + .to.eventually.equal('012') }) - it('should support increment multiple times', function () { - var src = '{% increment foo %}{%increment foo%}{{foo}}' - var ctx = { - foo: 1 - } - return expect(liquid.parseAndRender(src, ctx)) - .to.eventually.equal('3') + it('should increment defined variable', function () { + let src = '{% increment one %}{% increment one %}{% increment one %}' + let ctx = {one: 7} + return liquid.parseAndRender(src, ctx) + .then(x => { + expect(x).to.equal('789') + expect(ctx.one).to.equal(10) + }) + }) + + it('should be independent from assign', function () { + let src = '{% assign var=10 %}{% increment var %}{% increment var %}{% increment var %}' + return expect(liquid.parseAndRender(src)) + .to.eventually.equal('012') + }) + + it('should not shading assign', function () { + let src = '{% assign var=10 %}{% increment var %}{% increment var %}{% increment var %} {{var}}' + return expect(liquid.parseAndRender(src)) + .to.eventually.equal('012 10') }) }) diff --git a/test/util/error.js b/test/util/error.js index b54dce2be..a09a053dc 100644 --- a/test/util/error.js +++ b/test/util/error.js @@ -260,6 +260,7 @@ describe('error', function () { .be.rejected .then(function (err) { mock.restore() + console.log(err, err.name) expect(err.name).to.equal('RenderError') expect(err.file).to.equal(path.resolve('/foo.html')) })