refactor: src/scope.js

This commit is contained in:
harttle
2018-07-21 17:02:18 +08:00
parent c79700ef1a
commit 63b5658604
5 changed files with 88 additions and 128 deletions
+54 -86
View File
@@ -1,38 +1,50 @@
'use strict'
const _ = require('./util/underscore.js') const _ = require('./util/underscore.js')
const lexical = require('./lexical.js') const lexical = require('./lexical.js')
const assert = require('./util/assert.js') const assert = require('./util/assert.js')
var Scope = { var Scope = {
getAll: function () { getAll: function () {
var ctx = {} return this.scopes.reduce((ctx, val) => Object.assign(ctx, val), Object.create(null))
for (var i = this.scopes.length - 1; i >= 0; i--) {
_.assign(ctx, this.scopes[i])
}
return ctx
}, },
get: function (str) { get: function (path) {
try { let paths = this.propertyAccessSeq(path)
return this.getPropertyByPath(this.scopes, str) let scope = this.findScopeFor(paths[0])
} catch (e) { return paths.reduce((value, key) => this.readProperty(value, key), scope)
if (!/undefined variable/.test(e.message) || this.opts.strict_variables) { },
throw e set: function (path, v) {
let paths = this.propertyAccessSeq(path)
let scope = this.findScopeFor(paths[0])
paths.some((key, i) => {
if (!_.isObject(scope)) {
return true
} }
} if (i === paths.length - 1) {
}, scope[key] = v
set: function (k, v) { return true
var scope = this.findScopeFor(k) }
setPropertyByPath(scope, k, v) if (undefined === scope[key]) {
return this scope[key] = {}
}
scope = scope[key]
})
}, },
push: function (ctx) { push: function (ctx) {
assert(ctx, `trying to push ${ctx} into scopes`) assert(ctx, `trying to push ${ctx} into scopes`)
return this.scopes.push(ctx) this.scopes.push(ctx)
}, },
pop: function () { pop: function (ctx) {
return this.scopes.pop() if (!arguments.length) {
return this.scopes.pop()
}
let i = this.scopes.findIndex(scope => scope === ctx)
if (i === -1) {
throw new TypeError('scope not found, cannot pop')
}
return this.scopes.splice(i, 1)[0]
}, },
findScopeFor: function (key) { findScopeFor: function (key) {
var i = this.scopes.length - 1 let i = this.scopes.length - 1
while (i >= 0 && !(key in this.scopes[i])) { while (i >= 0 && !(key in this.scopes[i])) {
i-- i--
} }
@@ -41,32 +53,19 @@ var Scope = {
} }
return this.scopes[i] return this.scopes[i]
}, },
unshift: function (ctx) { readProperty: function (obj, key) {
assert(ctx, `trying to push ${ctx} into scopes`) let val
return this.scopes.unshift(ctx) if (key === 'size' && (_.isArray(obj) || _.isString(obj))) {
}, val = obj.length
shift: function () { } else if (_.isNil(obj)) {
return this.scopes.shift() val = undefined
}, } else {
val = obj[key]
getPropertyByPath: function (scopes, path) {
var paths = this.propertyAccessSeq(path + '')
if (!paths.length) {
throw new TypeError('undefined variable: ' + path)
} }
var key = paths.shift() if (_.isNil(val) && this.opts.strict_variables) {
var value = getValueFromScopes(key, scopes) throw new TypeError(`undefined variable: ${key}`)
if (_.isNil(value)) {
throw new TypeError('undefined variable: ' + key)
} }
while (paths.length) { return val
key = paths.shift()
value = getValueFromParent(key, value)
if (_.isNil(value)) {
throw new TypeError('undefined variable: ' + key)
}
}
return value
}, },
/* /*
@@ -78,16 +77,17 @@ var Scope = {
* accessSeq("foo[bar.coo]") // ['foo', 'bar'], for bar.coo == 'bar' * accessSeq("foo[bar.coo]") // ['foo', 'bar'], for bar.coo == 'bar'
*/ */
propertyAccessSeq: function (str) { propertyAccessSeq: function (str) {
var seq = [] str = String(str)
var name = '' let seq = []
var j let name = ''
var i = 0 let j
let i = 0
while (i < str.length) { while (i < str.length) {
switch (str[i]) { switch (str[i]) {
case '[': case '[':
push() push()
var delemiter = str[i + 1] let delemiter = str[i + 1]
if (/['"]/.test(delemiter)) { // foo["bar"] if (/['"]/.test(delemiter)) { // foo["bar"]
j = str.indexOf(delemiter, i + 2) j = str.indexOf(delemiter, i + 2)
assert(j !== -1, `unbalanced ${delemiter}: ${str}`) assert(j !== -1, `unbalanced ${delemiter}: ${str}`)
@@ -115,6 +115,10 @@ var Scope = {
} }
} }
push() push()
if (!seq.length) {
throw new TypeError(`invalid path:"${str}"`)
}
return seq return seq
function push () { function push () {
@@ -124,42 +128,6 @@ var Scope = {
} }
} }
function setPropertyByPath (obj, path, val) {
var paths = (path + '').replace(/\[/g, '.').replace(/\]/g, '').split('.')
for (var i = 0; i < paths.length; i++) {
var key = paths[i]
if (!_.isObject(obj)) {
// cannot set property of non-object
return
}
// for end point
if (i === paths.length - 1) {
return (obj[key] = val)
}
// if path not exist
if (undefined === obj[key]) {
obj[key] = {}
}
obj = obj[key]
}
}
function getValueFromParent (key, value) {
return (key === 'size' && (_.isArray(value) || _.isString(value)))
? value.length
: value[key]
}
function getValueFromScopes (key, scopes) {
for (var i = scopes.length - 1; i > -1; i--) {
var scope = scopes[i]
if (scope.hasOwnProperty(key)) {
return scope[key]
}
}
throw new TypeError('undefined variable: ' + key)
}
function matchRightBracket (str, begin) { function matchRightBracket (str, begin) {
var stack = 1 // count of '[' - count of ']' var stack = 1 // count of '[' - count of ']'
for (var i = begin; i < str.length; i++) { for (var i = begin; i < str.length; i++) {
+1 -10
View File
@@ -57,19 +57,10 @@ function forOwn (object, iteratee) {
function assign (object) { function assign (object) {
object = isObject(object) ? object : {} object = isObject(object) ? object : {}
var srcs = Array.prototype.slice.call(arguments, 1) var srcs = Array.prototype.slice.call(arguments, 1)
srcs.forEach(function (src) { srcs.forEach((src) => Object.assign(object, src))
_assignBinary(object, src)
})
return object return object
} }
function _assignBinary (dst, src) {
forOwn(src, function (v, k) {
dst[k] = v
})
return dst
}
function last (arr) { function last (arr) {
return arr[arr.length - 1] return arr[arr.length - 1]
} }
+1 -1
View File
@@ -84,7 +84,7 @@ module.exports = function (liquid) {
} }
throw e throw e
}) })
.then(() => scope.pop()) .then(() => scope.pop(context))
}).catch((e) => { }).catch((e) => {
if (e instanceof RenderBreakError && e.message === 'break') { if (e instanceof RenderBreakError && e.message === 'break') {
return return
+31 -31
View File
@@ -59,10 +59,16 @@ describe('scope', function () {
} }
expect(fn).to.not.throw() expect(fn).to.not.throw()
expect(scope.get('notdefined')).to.equal(undefined) expect(scope.get('notdefined')).to.equal(undefined)
expect(scope.get('')).to.equal(undefined)
expect(scope.get(false)).to.equal(undefined) expect(scope.get(false)).to.equal(undefined)
}) })
it('should throw for invalid path', function () {
function fn () {
scope.get('')
}
expect(fn).to.throw('invalid path:""')
})
it('should throw when [] unbalanced', function () { it('should throw when [] unbalanced', function () {
expect(function () { expect(function () {
scope.get('foo[bar') scope.get('foo[bar')
@@ -116,10 +122,9 @@ describe('scope', function () {
expect(scope.get('posts[category.diary[0]].name'), 'A Nice Day') expect(scope.get('posts[category.diary[0]].name'), 'A Nice Day')
}) })
it('should create in parent scope if needed', function () { it('should create parent if needed', function () {
scope.push({}) scope.set('a.b.c.d', 'COO')
scope.set('bar.coo', 'COO') expect(scope.get('a.b.c.d')).to.equal('COO')
expect(scope.get('bar.coo')).to.equal('COO')
}) })
it('should keep other properties of parent', function () { it('should keep other properties of parent', function () {
scope.push({obj: {foo: 'FOO'}}) scope.push({obj: {foo: 'FOO'}})
@@ -151,7 +156,8 @@ describe('scope', function () {
} }
expect(fn).to.throw(/undefined variable: notdefined/) expect(fn).to.throw(/undefined variable: notdefined/)
}) })
it('should throw when parent not defined', function () { it('should throw when deep variable not exist', function () {
scope.set('foo', 'FOO')
function fn () { function fn () {
scope.get('foo.bar.not.defined') scope.get('foo.bar.not.defined')
} }
@@ -209,31 +215,25 @@ describe('scope', function () {
expect(scope.get('foo')).to.equal('zoo') expect(scope.get('foo')).to.equal('zoo')
}) })
}) })
it('should pop specified scope', function () {
describe('.unshift()', function () { let scope1 = {
it('should throw when trying to unshift non-object', function () { foo: 'foo'
expect(function () { }
scope.unshift(false) let scope2 = {
}).to.throw() bar: 'bar'
}) }
it('should unshift scope', function () { scope.push(scope1)
scope.unshift({ scope.push(scope2)
foo: 'blue', expect(scope.get('foo')).to.equal('foo')
foo1: 'foo1' expect(scope.get('bar')).to.equal('bar')
}) scope.pop(scope1)
expect(scope.get('foo')).to.equal('zoo') expect(scope.get('foo')).to.equal('zoo')
expect(scope.get('foo1')).to.equal('foo1') expect(scope.get('bar')).to.equal('bar')
})
}) })
describe('.shift()', function () { it('should throw when specified scope not found', function () {
it('should shift scope', function () { let scope1 = {
scope.unshift({ foo: 'foo'
foo: 'blue', }
foo1: 'foo1' expect(() => scope.pop(scope1)).to.throw('scope not found, cannot pop')
})
scope.shift()
expect(scope.get('foo')).to.equal('zoo')
expect(scope.get('foo1')).to.equal(undefined)
})
}) })
}) })
+1
View File
@@ -2,6 +2,7 @@ const Liquid = require('..')
const sinon = require('sinon') const sinon = require('sinon')
const chai = require('chai') const chai = require('chai')
const expect = chai.expect const expect = chai.expect
chai.use(require('chai-as-promised'))
describe('xhr', () => { describe('xhr', () => {
if (process.version.match(/^v(\d+)/)[1] < 8) { if (process.version.match(/^v(\d+)/)[1] < 8) {