fix: post-increment, pre-decrement working on #76

This commit is contained in:
harttle
2018-07-21 22:59:52 +08:00
parent b5cfd2d2c5
commit c3de3fbb19
12 changed files with 146 additions and 92 deletions
+12 -11
View File
@@ -1,15 +1,16 @@
'use strict'
const strftime = require('./src/util/strftime.js') const strftime = require('./src/util/strftime.js')
const _ = require('./src/util/underscore.js') const _ = require('./src/util/underscore.js')
const isTruthy = require('./src/syntax.js').isTruthy const isTruthy = require('./src/syntax.js').isTruthy
var escapeMap = { let escapeMap = {
'&': '&', '&': '&',
'<': '&lt;', '<': '&lt;',
'>': '&gt;', '>': '&gt;',
'"': '&#34;', '"': '&#34;',
"'": '&#39;' "'": '&#39;'
} }
var unescapeMap = { let unescapeMap = {
'&amp;': '&', '&amp;': '&',
'&lt;': '<', '&lt;': '<',
'&gt;': '>', '&gt;': '>',
@@ -17,14 +18,14 @@ var unescapeMap = {
'&#39;': "'" '&#39;': "'"
} }
var filters = { let filters = {
'abs': v => Math.abs(v), 'abs': v => Math.abs(v),
'append': (v, arg) => v + arg, 'append': (v, arg) => v + arg,
'capitalize': str => stringify(str).charAt(0).toUpperCase() + str.slice(1), 'capitalize': str => stringify(str).charAt(0).toUpperCase() + str.slice(1),
'ceil': v => Math.ceil(v), 'ceil': v => Math.ceil(v),
'concat': (v, arg) => Array.prototype.concat.call(v, arg), 'concat': (v, arg) => Array.prototype.concat.call(v, arg),
'date': (v, arg) => { 'date': (v, arg) => {
var date = v let date = v
if (v === 'now') { if (v === 'now') {
date = new Date() date = new Date()
} else if (_.isString(v)) { } else if (_.isString(v)) {
@@ -41,7 +42,7 @@ var filters = {
'first': v => v[0], 'first': v => v[0],
'floor': v => Math.floor(v), 'floor': v => Math.floor(v),
'join': (v, arg) => v.join(arg), 'join': (v, arg) => v.join(arg),
'last': v => v[v.length - 1], 'last': v => _.last(v),
'lstrip': v => stringify(v).replace(/^\s+/, ''), 'lstrip': v => stringify(v).replace(/^\s+/, ''),
'map': (arr, arg) => arr.map(v => v[arg]), 'map': (arr, arg) => arr.map(v => v[arg]),
'minus': bindFixed((v, arg) => v - arg), 'minus': bindFixed((v, arg) => v - arg),
@@ -56,7 +57,7 @@ var filters = {
'replace_first': (v, arg1, arg2) => stringify(v).replace(arg1, arg2), 'replace_first': (v, arg1, arg2) => stringify(v).replace(arg1, arg2),
'reverse': v => v.reverse(), 'reverse': v => v.reverse(),
'round': (v, arg) => { 'round': (v, arg) => {
var amp = Math.pow(10, arg || 0) let amp = Math.pow(10, arg || 0)
return Math.round(v * amp, arg) / amp return Math.round(v * amp, arg) / amp
}, },
'rstrip': str => stringify(str).replace(/\s+$/, ''), 'rstrip': str => stringify(str).replace(/\s+$/, ''),
@@ -78,13 +79,13 @@ var filters = {
}, },
'truncatewords': (v, l, o) => { 'truncatewords': (v, l, o) => {
if (o === undefined) o = '...' if (o === undefined) o = '...'
var arr = v.split(' ') let arr = v.split(' ')
var ret = arr.slice(0, l).join(' ') let ret = arr.slice(0, l).join(' ')
if (arr.length > l) ret += o if (arr.length > l) ret += o
return ret return ret
}, },
'uniq': function (arr) { 'uniq': function (arr) {
var u = {} let u = {}
return (arr || []).filter(val => { return (arr || []).filter(val => {
if (u.hasOwnProperty(val)) { if (u.hasOwnProperty(val)) {
return false return false
@@ -106,7 +107,7 @@ function unescape (str) {
} }
function getFixed (v) { function getFixed (v) {
var p = (v + '').split('.') let p = (v + '').split('.')
return (p.length > 1) ? p[1].length : 0 return (p.length > 1) ? p[1].length : 0
} }
@@ -120,7 +121,7 @@ function stringify (obj) {
function bindFixed (cb) { function bindFixed (cb) {
return (l, r) => { return (l, r) => {
var f = getMaxFixed(l, r) let f = getMaxFixed(l, r)
return cb(l, r).toFixed(f) return cb(l, r).toFixed(f)
} }
} }
+27 -16
View File
@@ -5,16 +5,16 @@ const assert = require('./util/assert.js')
var Scope = { var Scope = {
getAll: function () { 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) { get: function (path) {
let paths = this.propertyAccessSeq(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) return paths.reduce((value, key) => this.readProperty(value, key), scope)
}, },
set: function (path, v) { set: function (path, v) {
let paths = this.propertyAccessSeq(path) let paths = this.propertyAccessSeq(path)
let scope = this.findScopeFor(paths[0]) let scope = this.findContextFor(paths[0]) || _.last(this.contexts)
paths.some((key, i) => { paths.some((key, i) => {
if (!_.isObject(scope)) { if (!_.isObject(scope)) {
return true return true
@@ -29,28 +29,32 @@ var Scope = {
scope = scope[key] scope = scope[key]
}) })
}, },
unshift: function (ctx) {
return this.contexts.unshift(ctx)
},
push: function (ctx) { push: function (ctx) {
this.scopes.push(ctx) return this.contexts.push(ctx)
}, },
pop: function (ctx) { pop: function (ctx) {
if (!arguments.length) { 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) { if (i === -1) {
throw new TypeError('scope not found, cannot pop') 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) { findContextFor: function (key, filter) {
let i = this.scopes.length - 1 filter = filter || (() => true)
while (i >= 0 && !(key in this.scopes[i])) { for (let i = this.contexts.length - 1; i >= 0; i--) {
i-- let candidate = this.contexts[i]
if (!filter(candidate)) continue
if (key in candidate) {
return candidate
}
} }
if (i < 0) { return null
i = this.scopes.length - 1
}
return this.scopes[i]
}, },
readProperty: function (obj, key) { readProperty: function (obj, key) {
let val let val
@@ -153,6 +157,13 @@ exports.factory = function (ctx, opts) {
} }
var scope = Object.create(Scope) var scope = Object.create(Scope)
scope.opts = _.assign(defaultOptions, opts) scope.opts = _.assign(defaultOptions, opts)
scope.scopes = [ctx || {}] scope.contexts = [ctx || {}]
return scope return scope
} }
exports.types = {
AssignScope: Object.create(null),
CaptureScope: Object.create(null),
IncrementScope: Object.create(null),
DecrementScope: Object.create(null)
}
+11 -10
View File
@@ -1,26 +1,27 @@
'use strict'
const lexical = require('./lexical.js') const lexical = require('./lexical.js')
const Syntax = require('./syntax.js') const Syntax = require('./syntax.js')
const assert = require('./util/assert.js') const assert = require('./util/assert.js')
function hash (markup, scope) { function hash (markup, scope) {
var obj = {} let obj = {}
var match let match
lexical.hashCapture.lastIndex = 0 lexical.hashCapture.lastIndex = 0
while ((match = lexical.hashCapture.exec(markup))) { while ((match = lexical.hashCapture.exec(markup))) {
var k = match[1] let k = match[1]
var v = match[2] let v = match[2]
obj[k] = Syntax.evalValue(v, scope) obj[k] = Syntax.evalValue(v, scope)
} }
return obj return obj
} }
module.exports = function () { module.exports = function () {
var tagImpls = {} let tagImpls = {}
var _tagInstance = { let _tagInstance = {
render: function (scope) { render: function (scope) {
var obj = hash(this.token.args, scope) let obj = hash(this.token.args, scope)
var impl = this.tagImpl let impl = this.tagImpl
if (typeof impl.render !== 'function') { if (typeof impl.render !== 'function') {
return Promise.resolve('') return Promise.resolve('')
} }
@@ -31,7 +32,7 @@ module.exports = function () {
this.token = token this.token = token
this.name = token.name this.name = token.name
var tagImpl = tagImpls[this.name] let tagImpl = tagImpls[this.name]
assert(tagImpl, `tag ${this.name} not found`) assert(tagImpl, `tag ${this.name} not found`)
this.tagImpl = Object.create(tagImpl) this.tagImpl = Object.create(tagImpl)
if (this.tagImpl.parse) { if (this.tagImpl.parse) {
@@ -45,7 +46,7 @@ module.exports = function () {
} }
function construct (token, tokens) { function construct (token, tokens) {
var instance = Object.create(_tagInstance) let instance = Object.create(_tagInstance)
instance.parse(token, tokens) instance.parse(token, tokens)
return instance return instance
} }
+2 -1
View File
@@ -1,6 +1,7 @@
const resolve = require('resolve-url') const resolve = require('resolve-url')
const splitPathRe = /^(\/?|)([\s\S]*?)((?:\.{1,2}|[^/]+?|)(\.[^./]*|))(?:[/]*)$/ const splitPathRe = /^(\/?|)([\s\S]*?)((?:\.{1,2}|[^/]+?|)(\.[^./]*|))(?:[/]*)$/
const urlRe = /^(?:\w+:)?\/\/([^\s.]+\.\S{2}|localhost[:?\d]*)\S*$/ const urlRe = /^(?:\w+:)?\/\/([^\s.]+\.\S{2}|localhost[:?\d]*)\S*$/
const _ = require('./underscore')
// https://github.com/jinder/path/blob/master/path.js#L567 // https://github.com/jinder/path/blob/master/path.js#L567
exports.extname = function (path) { exports.extname = function (path) {
@@ -16,7 +17,7 @@ exports.resolve = function (root, path) {
if (Object.prototype.toString.call(root) === '[object Array]') { if (Object.prototype.toString.call(root) === '[object Array]') {
root = root[0] root = root[0]
} }
if (root && root.charAt(root.length - 1) !== '/') { if (root && _.last(root) !== '/') {
root += '/' root += '/'
} }
return resolve(root, path) return resolve(root, path)
+2 -1
View File
@@ -3,6 +3,7 @@ const Liquid = require('..')
const lexical = Liquid.lexical const lexical = Liquid.lexical
const re = new RegExp(`(${lexical.identifier.source})\\s*=(.*)`) const re = new RegExp(`(${lexical.identifier.source})\\s*=(.*)`)
const assert = require('../src/util/assert.js') const assert = require('../src/util/assert.js')
const types = require('../src/scope').types
module.exports = function (liquid) { module.exports = function (liquid) {
liquid.registerTag('assign', { liquid.registerTag('assign', {
@@ -13,7 +14,7 @@ module.exports = function (liquid) {
this.value = match[2] this.value = match[2]
}, },
render: function (scope) { render: function (scope) {
let ctx = Object.create(null) let ctx = Object.create(types.AssignScope)
ctx[this.key] = liquid.evalValue(this.value, scope) ctx[this.key] = liquid.evalValue(this.value, scope)
scope.push(ctx) scope.push(ctx)
return Promise.resolve('') return Promise.resolve('')
+17 -3
View File
@@ -1,6 +1,8 @@
'use strict'
const Liquid = require('..') const Liquid = require('..')
const lexical = Liquid.lexical const lexical = Liquid.lexical
const assert = require('../src/util/assert.js') const assert = require('../src/util/assert.js')
const types = require('../src/scope').types
module.exports = function (liquid) { module.exports = function (liquid) {
liquid.registerTag('decrement', { liquid.registerTag('decrement', {
@@ -10,9 +12,21 @@ module.exports = function (liquid) {
this.variable = match[0] this.variable = match[0]
}, },
render: function (scope, hash) { render: function (scope, hash) {
var v = scope.get(this.variable) let context = scope.findContextFor(
if (typeof v !== 'number') v = 0 this.variable,
scope.set(this.variable, v - 1) 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]
} }
}) })
} }
+20 -4
View File
@@ -1,18 +1,34 @@
'use strict'
const Liquid = require('..') const Liquid = require('..')
const assert = require('../src/util/assert.js') const assert = require('../src/util/assert.js')
const lexical = Liquid.lexical const lexical = Liquid.lexical
const types = require('../src/scope').types
module.exports = function (liquid) { module.exports = function (liquid) {
liquid.registerTag('increment', { liquid.registerTag('increment', {
parse: function (token) { parse: function (token) {
var match = token.args.match(lexical.identifier) let match = token.args.match(lexical.identifier)
assert(match, `illegal identifier ${token.args}`) assert(match, `illegal identifier ${token.args}`)
this.variable = match[0] this.variable = match[0]
}, },
render: function (scope, hash) { render: function (scope, hash) {
var v = scope.get(this.variable) let context = scope.findContextFor(
if (typeof v !== 'number') v = 0 this.variable,
scope.set(this.variable, v + 1) 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
} }
}) })
} }
+2 -3
View File
@@ -9,14 +9,13 @@ module.exports = function (liquid) {
if (token.name === 'endraw') stream.stop() if (token.name === 'endraw') stream.stop()
else this.tokens.push(token) else this.tokens.push(token)
}) })
.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 tokens = this.tokens.map(token => token.raw).join('') return this.tokens.map(token => token.raw).join('')
return Promise.resolve(tokens)
} }
}) })
} }
+2 -2
View File
@@ -141,7 +141,7 @@ describe('filters', function () {
it('should support split/first', function () { it('should support split/first', function () {
var src = '{% assign my_array = "apples, oranges, peaches, plums" | split: ", " %}' + var src = '{% assign my_array = "apples, oranges, peaches, plums" | split: ", " %}' +
'{{ my_array | first }}' '{{ my_array | first }}'
return test(src, 'apples') return test(src, 'apples')
}) })
@@ -160,7 +160,7 @@ describe('filters', function () {
it('should support split/last', function () { it('should support split/last', function () {
var src = '{% assign my_array = "zebra, octopus, giraffe, tiger" | split: ", " %}' + var src = '{% assign my_array = "zebra, octopus, giraffe, tiger" | split: ", " %}' +
'{{ my_array|last }}' '{{ my_array|last }}'
return test(src, 'tiger') return test(src, 'tiger')
}) })
+25 -21
View File
@@ -1,3 +1,4 @@
'use strict'
const Liquid = require('../..') const Liquid = require('../..')
const chai = require('chai') const chai = require('chai')
const expect = chai.expect const expect = chai.expect
@@ -5,34 +6,37 @@ chai.use(require('chai-as-promised'))
describe('tags/decrement', function () { describe('tags/decrement', function () {
var liquid = Liquid() var liquid = Liquid()
it('should throw when variable expression illegal', function () { it('should throw when variable expression illegal', function () {
var src = '{% decrement / %}{{one}}' var src = '{% decrement / %}{{var}}'
var ctx = {} var ctx = {}
return expect(liquid.parseAndRender(src, ctx)).to.be.rejectedWith(/illegal/) return expect(liquid.parseAndRender(src, ctx)).to.be.rejectedWith(/illegal/)
}) })
it('should support decrement', function () { it('should decrement undefined variable', function () {
var src = '{% decrement one %}{{one}}' let src = '{% decrement var %}{% decrement var %}{% decrement var %}'
var ctx = {
one: 1
}
return expect(liquid.parseAndRender(src, ctx))
.to.eventually.equal('0')
})
it('should decrement undefined', function () {
var src = '{% decrement empty %}{{empty}}'
return expect(liquid.parseAndRender(src)) return expect(liquid.parseAndRender(src))
.to.eventually.equal('-1') .to.eventually.equal('-1-2-3')
}) })
it('should support decrement multiple times', function () { it('should decrement defined variable', function () {
var src = '{% decrement foo %}{%decrement foo%}{{foo}}' let src = '{% decrement var %}{% decrement var %}{% decrement var %}'
var ctx = { let ctx = {'var': 10}
foo: 1 return liquid.parseAndRender(src, ctx)
} .then(x => {
return expect(liquid.parseAndRender(src, ctx)) expect(x).to.equal('987')
.to.eventually.equal('-1') 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')
}) })
}) })
+25 -20
View File
@@ -1,32 +1,37 @@
'use strict'
const Liquid = require('../..') const Liquid = require('../..')
const chai = require('chai') const chai = require('chai')
const expect = chai.expect const expect = chai.expect
chai.use(require('chai-as-promised')) chai.use(require('chai-as-promised'))
describe('tags/increment', function () { describe('tags/increment', function () {
var liquid = Liquid() let liquid = Liquid()
it('should support increment', function () { it('should increment undefined variable', function () {
var src = '{% increment one %}{{one}}' let src = '{% increment one %}{% increment one %}{% increment 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}}'
return expect(liquid.parseAndRender(src)) return expect(liquid.parseAndRender(src))
.to.eventually.equal('1') .to.eventually.equal('012')
}) })
it('should support increment multiple times', function () { it('should increment defined variable', function () {
var src = '{% increment foo %}{%increment foo%}{{foo}}' let src = '{% increment one %}{% increment one %}{% increment one %}'
var ctx = { let ctx = {one: 7}
foo: 1 return liquid.parseAndRender(src, ctx)
} .then(x => {
return expect(liquid.parseAndRender(src, ctx)) expect(x).to.equal('789')
.to.eventually.equal('3') 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')
}) })
}) })
+1
View File
@@ -260,6 +260,7 @@ describe('error', function () {
.be.rejected .be.rejected
.then(function (err) { .then(function (err) {
mock.restore() mock.restore()
console.log(err, err.name)
expect(err.name).to.equal('RenderError') expect(err.name).to.equal('RenderError')
expect(err.file).to.equal(path.resolve('/foo.html')) expect(err.file).to.equal(path.resolve('/foo.html'))
}) })