refactor: switch Context <-> Scope concepts

This commit is contained in:
harttle
2019-03-25 10:36:23 +08:00
parent 76345a64c3
commit 45e3c2bb8e
34 changed files with 272 additions and 272 deletions
+8
View File
@@ -0,0 +1,8 @@
enum BlockMode {
/* store rendered html into blocks */
OUTPUT,
/* output rendered html directly */
STORE
}
export default BlockMode
+148
View File
@@ -0,0 +1,148 @@
import * as _ from '../util/underscore'
import { Drop } from '../drop/drop'
import { __assign } from 'tslib'
import assert from '../util/assert'
import { NormalizedFullOptions, applyDefault } from '../liquid-options'
import BlockMode from './block-mode'
import { Scope } from './scope'
export default class Context {
opts: NormalizedFullOptions
scopes: Array<Scope> = [{}]
environments: Scope
blocks: object = {}
groups: {[key: string]: number} = {}
blockMode: BlockMode = BlockMode.OUTPUT
constructor (ctx: object = {}, opts?: NormalizedFullOptions) {
this.opts = applyDefault(opts)
this.environments = ctx
}
getAll () {
return [this.environments, ...this.scopes]
.reduce((ctx, val) => __assign(ctx, val), {})
}
async get (path: string) {
const paths = await this.propertyAccessSeq(path)
let ctx = this.findContextFor(paths[0]) || this.environments
for (const path of paths) {
ctx = this.readProperty(ctx, path)
if (_.isNil(ctx) && this.opts.strictVariables) {
throw new TypeError(`undefined variable: ${path}`)
}
}
return ctx
}
push (ctx: object) {
return this.scopes.push(ctx)
}
pop (ctx?: object): object | undefined {
if (!arguments.length) {
return this.scopes.pop()
}
const 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]
}
findContextFor (key: string) {
for (let i = this.scopes.length - 1; i >= 0; i--) {
const candidate = this.scopes[i]
if (key in candidate) {
return candidate
}
}
return null
}
private readProperty (obj: Scope, key: string) {
if (_.isNil(obj)) return obj
obj = _.toLiquid(obj)
if (obj instanceof Drop) {
if (_.isFunction(obj[key])) return obj[key]()
if (obj.hasOwnProperty(key)) return obj[key]
return obj.liquidMethodMissing(key)
}
return key === 'size' ? readSize(obj) : obj[key]
}
/*
* Parse property access sequence from access string
* @example
* accessSeq("foo.bar") // ['foo', 'bar']
* accessSeq("foo['bar']") // ['foo', 'bar']
* accessSeq("foo['b]r']") // ['foo', 'b]r']
* accessSeq("foo[bar.coo]") // ['foo', 'bar'], for bar.coo == 'bar'
*/
async propertyAccessSeq (str: string) {
str = String(str)
const seq: string[] = []
let name = ''
let j
let i = 0
while (i < str.length) {
switch (str[i]) {
case '[':
push()
const delemiter = str[i + 1]
if (/['"]/.test(delemiter)) { // foo["bar"]
j = str.indexOf(delemiter, i + 2)
assert(j !== -1, `unbalanced ${delemiter}: ${str}`)
name = str.slice(i + 2, j)
push()
i = j + 2
} else { // foo[bar.coo]
j = matchRightBracket(str, i + 1)
assert(j !== -1, `unbalanced []: ${str}`)
name = str.slice(i + 1, j)
if (!/^[+-]?\d+$/.test(name)) { // foo[bar] vs. foo[1]
name = String(await this.get(name))
}
push()
i = j + 1
}
break
case '.':// foo.bar, foo[0].bar
push()
i++
break
default:// foo.bar
name += str[i]
i++
}
}
push()
if (!seq.length) {
throw new TypeError(`invalid path:"${str}"`)
}
return seq
function push () {
if (name.length) seq.push(name)
name = ''
}
}
}
function readSize (obj: Scope) {
if (!_.isNil(obj['size'])) return obj['size']
if (_.isArray(obj) || _.isString(obj)) return obj.length
return obj['size']
}
function matchRightBracket (str: string, begin: number) {
let stack = 1 // count of '[' - count of ']'
for (let i = begin; i < str.length; i++) {
if (str[i] === '[') {
stack++
}
if (str[i] === ']') {
stack--
if (stack === 0) {
return i
}
}
}
return -1
}
+8
View File
@@ -0,0 +1,8 @@
import { Drop } from '../drop/drop'
type PlainObject = {
[key: string]: any
toLiquid?: () => any
}
export type Scope = PlainObject | Drop