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
+3 -3
View File
@@ -1,7 +1,7 @@
import * as Benchmark from 'benchmark' import * as Benchmark from 'benchmark'
import Liquid from '../src/liquid' import Liquid from '../src/liquid'
import TagToken from '../src/parser/tag-token' import TagToken from '../src/parser/tag-token'
import Scope from '../src/scope/scope' import Context from '../src/context/context'
const engine = new Liquid({ const engine = new Liquid({
root: __dirname, root: __dirname,
@@ -13,8 +13,8 @@ engine.registerTag('header', {
const [key, val] = token.args.split(':') const [key, val] = token.args.split(':')
this[key] = val this[key] = val
}, },
render: function (scope: Scope) { render: function (ctx: Context) {
const title = this.liquid.evalValue(this.content, scope) const title = this.liquid.evalValue(this.content, ctx)
return `<h1>${title}</h1>` return `<h1>${title}</h1>`
} }
}) })
+3 -3
View File
@@ -1,7 +1,7 @@
import assert from '../../util/assert' import assert from '../../util/assert'
import { identifier } from '../../parser/lexical' import { identifier } from '../../parser/lexical'
import TagToken from '../../parser/tag-token' import TagToken from '../../parser/tag-token'
import Scope from '../../scope/scope' import Context from '../../context/context'
import ITagImplOptions from '../../template/tag/itag-impl-options' import ITagImplOptions from '../../template/tag/itag-impl-options'
const re = new RegExp(`(${identifier.source})\\s*=([^]*)`) const re = new RegExp(`(${identifier.source})\\s*=([^]*)`)
@@ -13,7 +13,7 @@ export default {
this.key = match[1] this.key = match[1]
this.value = match[2] this.value = match[2]
}, },
render: async function (scope: Scope) { render: async function (ctx: Context) {
scope.contexts[0][this.key] = await this.liquid.evalValue(this.value, scope) ctx.scopes[0][this.key] = await this.liquid.evalValue(this.value, ctx)
} }
} as ITagImplOptions } as ITagImplOptions
+7 -7
View File
@@ -1,8 +1,8 @@
import BlockMode from '../../scope/block-mode' import BlockMode from '../../context/block-mode'
import TagToken from '../../parser/tag-token' import TagToken from '../../parser/tag-token'
import Token from '../../parser/token' import Token from '../../parser/token'
import ITemplate from '../../template/itemplate' import ITemplate from '../../template/itemplate'
import Scope from '../../scope/scope' import Context from '../../context/context'
import ITagImplOptions from '../../template/tag/itag-impl-options' import ITagImplOptions from '../../template/tag/itag-impl-options'
import ParseStream from '../../parser/parse-stream' import ParseStream from '../../parser/parse-stream'
@@ -19,14 +19,14 @@ export default {
}) })
stream.start() stream.start()
}, },
render: async function (scope: Scope) { render: async function (ctx: Context) {
const childDefined = scope.blocks[this.block] const childDefined = ctx.blocks[this.block]
const html = childDefined !== undefined const html = childDefined !== undefined
? childDefined ? childDefined
: await this.liquid.renderer.renderTemplates(this.tpls, scope) : await this.liquid.renderer.renderTemplates(this.tpls, ctx)
if (scope.blockMode === BlockMode.STORE) { if (ctx.blockMode === BlockMode.STORE) {
scope.blocks[this.block] = html ctx.blocks[this.block] = html
return '' return ''
} }
return html return html
+4 -4
View File
@@ -2,7 +2,7 @@ import assert from '../../util/assert'
import { identifier } from '../../parser/lexical' import { identifier } from '../../parser/lexical'
import TagToken from '../../parser/tag-token' import TagToken from '../../parser/tag-token'
import Token from '../../parser/token' import Token from '../../parser/token'
import Scope from '../../scope/scope' import Context from '../../context/context'
import ITagImplOptions from '../../template/tag/itag-impl-options' import ITagImplOptions from '../../template/tag/itag-impl-options'
const re = new RegExp(`(${identifier.source})`) const re = new RegExp(`(${identifier.source})`)
@@ -23,8 +23,8 @@ export default {
}) })
stream.start() stream.start()
}, },
render: async function (scope: Scope) { render: async function (ctx: Context) {
const html = await this.liquid.renderer.renderTemplates(this.templates, scope) const html = await this.liquid.renderer.renderTemplates(this.templates, ctx)
scope.contexts[0][this.variable] = html ctx.scopes[0][this.variable] = html
} }
} as ITagImplOptions } as ITagImplOptions
+6 -6
View File
@@ -1,7 +1,7 @@
import { evalExp } from '../../render/syntax' import { evalExp } from '../../render/syntax'
import TagToken from '../../parser/tag-token' import TagToken from '../../parser/tag-token'
import Token from '../../parser/token' import Token from '../../parser/token'
import Scope from '../../scope/scope' import Context from '../../context/context'
import ITemplate from '../../template/itemplate' import ITemplate from '../../template/itemplate'
import ITagImplOptions from '../../template/tag/itag-impl-options' import ITagImplOptions from '../../template/tag/itag-impl-options'
import ParseStream from '../../parser/parse-stream' import ParseStream from '../../parser/parse-stream'
@@ -30,15 +30,15 @@ export default {
stream.start() stream.start()
}, },
render: async function (scope: Scope) { render: async function (ctx: Context) {
for (let i = 0; i < this.cases.length; i++) { for (let i = 0; i < this.cases.length; i++) {
const branch = this.cases[i] const branch = this.cases[i]
const val = await evalExp(branch.val, scope) const val = await evalExp(branch.val, ctx)
const cond = await evalExp(this.cond, scope) const cond = await evalExp(this.cond, ctx)
if (val === cond) { if (val === cond) {
return this.liquid.renderer.renderTemplates(branch.templates, scope) return this.liquid.renderer.renderTemplates(branch.templates, ctx)
} }
} }
return this.liquid.renderer.renderTemplates(this.elseTemplates, scope) return this.liquid.renderer.renderTemplates(this.elseTemplates, ctx)
} }
} as ITagImplOptions } as ITagImplOptions
+5 -5
View File
@@ -2,7 +2,7 @@ import assert from '../../util/assert'
import { value as rValue } from '../../parser/lexical' import { value as rValue } from '../../parser/lexical'
import { evalValue } from '../../render/syntax' import { evalValue } from '../../render/syntax'
import TagToken from '../../parser/tag-token' import TagToken from '../../parser/tag-token'
import Scope from '../../scope/scope' import Context from '../../context/context'
import ITagImplOptions from '../../template/tag/itag-impl-options' import ITagImplOptions from '../../template/tag/itag-impl-options'
const groupRE = new RegExp(`^(?:(${rValue.source})\\s*:\\s*)?(.*)$`) const groupRE = new RegExp(`^(?:(${rValue.source})\\s*:\\s*)?(.*)$`)
@@ -24,10 +24,10 @@ export default <ITagImplOptions>{
assert(this.candidates.length, `empty candidates: ${tagToken.raw}`) assert(this.candidates.length, `empty candidates: ${tagToken.raw}`)
}, },
render: async function (scope: Scope) { render: async function (ctx: Context) {
const group = await evalValue(this.group, scope) const group = await evalValue(this.group, ctx)
const fingerprint = `cycle:${group}:` + this.candidates.join(',') const fingerprint = `cycle:${group}:` + this.candidates.join(',')
const groups = scope.groups const groups = ctx.groups
let idx = groups[fingerprint] let idx = groups[fingerprint]
if (idx === undefined) { if (idx === undefined) {
@@ -38,6 +38,6 @@ export default <ITagImplOptions>{
idx = (idx + 1) % this.candidates.length idx = (idx + 1) % this.candidates.length
groups[fingerprint] = idx groups[fingerprint] = idx
return evalValue(candidate, scope) return evalValue(candidate, ctx)
} }
} }
+2 -2
View File
@@ -1,7 +1,7 @@
import assert from '../../util/assert' import assert from '../../util/assert'
import { identifier } from '../../parser/lexical' import { identifier } from '../../parser/lexical'
import TagToken from '../../parser/tag-token' import TagToken from '../../parser/tag-token'
import Scope from '../../scope/scope' import Context from '../../context/context'
import ITagImplOptions from '../../template/tag/itag-impl-options' import ITagImplOptions from '../../template/tag/itag-impl-options'
export default { export default {
@@ -10,7 +10,7 @@ export default {
assert(match, `illegal identifier ${token.args}`) assert(match, `illegal identifier ${token.args}`)
this.variable = match[0] this.variable = match[0]
}, },
render: function (context: Scope) { render: function (context: Context) {
const scope = context.environments const scope = context.environments
if (typeof scope[this.variable] !== 'number') { if (typeof scope[this.variable] !== 'number') {
scope[this.variable] = 0 scope[this.variable] = 0
+7 -7
View File
@@ -4,7 +4,7 @@ import assert from '../../util/assert'
import { identifier, value, hash } from '../../parser/lexical' import { identifier, value, hash } from '../../parser/lexical'
import TagToken from '../../parser/tag-token' import TagToken from '../../parser/tag-token'
import Token from '../../parser/token' import Token from '../../parser/token'
import Scope from '../../scope/scope' import Context from '../../context/context'
import Hash from '../../template/tag/hash' import Hash from '../../template/tag/hash'
import ITemplate from '../../template/itemplate' import ITemplate from '../../template/itemplate'
import ITagImplOptions from '../../template/tag/itag-impl-options' import ITagImplOptions from '../../template/tag/itag-impl-options'
@@ -41,8 +41,8 @@ export default <ITagImplOptions>{
stream.start() stream.start()
}, },
render: async function (scope: Scope, hash: Hash) { render: async function (ctx: Context, hash: Hash) {
let collection = await evalExp(this.collection, scope) let collection = await evalExp(this.collection, ctx)
if (!isArray(collection)) { if (!isArray(collection)) {
if (isString(collection) && collection.length > 0) { if (isString(collection) && collection.length > 0) {
@@ -52,7 +52,7 @@ export default <ITagImplOptions>{
} }
} }
if (!isArray(collection) || !collection.length) { if (!isArray(collection) || !collection.length) {
return this.liquid.renderer.renderTemplates(this.elseTemplates, scope) return this.liquid.renderer.renderTemplates(this.elseTemplates, ctx)
} }
const offset = hash.offset || 0 const offset = hash.offset || 0
@@ -62,12 +62,12 @@ export default <ITagImplOptions>{
if (this.reversed) collection.reverse() if (this.reversed) collection.reverse()
const context = { forloop: new ForloopDrop(collection.length) } const context = { forloop: new ForloopDrop(collection.length) }
scope.push(context) ctx.push(context)
let html = '' let html = ''
for (const item of collection) { for (const item of collection) {
context[this.variable] = item context[this.variable] = item
try { try {
html += await this.liquid.renderer.renderTemplates(this.templates, scope) html += await this.liquid.renderer.renderTemplates(this.templates, ctx)
} catch (e) { } catch (e) {
if (e.name === 'RenderBreakError') { if (e.name === 'RenderBreakError') {
html += e.resolvedHTML html += e.resolvedHTML
@@ -76,7 +76,7 @@ export default <ITagImplOptions>{
} }
context.forloop.next() context.forloop.next()
} }
scope.pop() ctx.pop()
return html return html
} }
} }
+5 -5
View File
@@ -1,7 +1,7 @@
import { evalExp, isTruthy } from '../../render/syntax' import { evalExp, isTruthy } from '../../render/syntax'
import TagToken from '../../parser/tag-token' import TagToken from '../../parser/tag-token'
import Token from '../../parser/token' import Token from '../../parser/token'
import Scope from '../../scope/scope' import Context from '../../context/context'
import ITemplate from '../../template/itemplate' import ITemplate from '../../template/itemplate'
import ITagImplOptions from '../../template/tag/itag-impl-options' import ITagImplOptions from '../../template/tag/itag-impl-options'
import ParseStream from '../../parser/parse-stream' import ParseStream from '../../parser/parse-stream'
@@ -33,13 +33,13 @@ export default {
stream.start() stream.start()
}, },
render: async function (scope: Scope) { render: async function (ctx: Context) {
for (const branch of this.branches) { for (const branch of this.branches) {
const cond = await evalExp(branch.cond, scope) const cond = await evalExp(branch.cond, ctx)
if (isTruthy(cond)) { if (isTruthy(cond)) {
return this.liquid.renderer.renderTemplates(branch.templates, scope) return this.liquid.renderer.renderTemplates(branch.templates, ctx)
} }
} }
return this.liquid.renderer.renderTemplates(this.elseTemplates, scope) return this.liquid.renderer.renderTemplates(this.elseTemplates, ctx)
} }
} as ITagImplOptions } as ITagImplOptions
+17 -17
View File
@@ -1,9 +1,9 @@
import assert from '../../util/assert' import assert from '../../util/assert'
import { value, quotedLine } from '../../parser/lexical' import { value, quotedLine } from '../../parser/lexical'
import { evalValue } from '../../render/syntax' import { evalValue } from '../../render/syntax'
import BlockMode from '../../scope/block-mode' import BlockMode from '../../context/block-mode'
import TagToken from '../../parser/tag-token' import TagToken from '../../parser/tag-token'
import Scope from '../../scope/scope' import Context from '../../context/context'
import Hash from '../../template/tag/hash' import Hash from '../../template/tag/hash'
import ITagImplOptions from '../../template/tag/itag-impl-options' import ITagImplOptions from '../../template/tag/itag-impl-options'
@@ -27,34 +27,34 @@ export default <ITagImplOptions>{
this.with = match[1] this.with = match[1]
} }
}, },
render: async function (scope: Scope, hash: Hash) { render: async function (ctx: Context, hash: Hash) {
let filepath let filepath
if (scope.opts.dynamicPartials) { if (ctx.opts.dynamicPartials) {
if (quotedLine.exec(this.value)) { if (quotedLine.exec(this.value)) {
const template = this.value.slice(1, -1) const template = this.value.slice(1, -1)
filepath = await this.liquid.parseAndRender(template, scope.getAll(), scope.opts) filepath = await this.liquid.parseAndRender(template, ctx.getAll(), ctx.opts)
} else { } else {
filepath = await evalValue(this.value, scope) filepath = await evalValue(this.value, ctx)
} }
} else { } else {
filepath = this.staticValue filepath = this.staticValue
} }
assert(filepath, `cannot include with empty filename`) assert(filepath, `cannot include with empty filename`)
const originBlocks = scope.blocks const originBlocks = ctx.blocks
const originBlockMode = scope.blockMode const originBlockMode = ctx.blockMode
scope.blocks = {} ctx.blocks = {}
scope.blockMode = BlockMode.OUTPUT ctx.blockMode = BlockMode.OUTPUT
if (this.with) { if (this.with) {
hash[filepath] = await evalValue(this.with, scope) hash[filepath] = await evalValue(this.with, ctx)
} }
const templates = await this.liquid.getTemplate(filepath, scope.opts) const templates = await this.liquid.getTemplate(filepath, ctx.opts)
scope.push(hash) ctx.push(hash)
const html = await this.liquid.renderer.renderTemplates(templates, scope) const html = await this.liquid.renderer.renderTemplates(templates, ctx)
scope.pop(hash) ctx.pop(hash)
scope.blocks = originBlocks ctx.blocks = originBlocks
scope.blockMode = originBlockMode ctx.blockMode = originBlockMode
return html return html
} }
} }
+14 -14
View File
@@ -1,10 +1,10 @@
import assert from '../../util/assert' import assert from '../../util/assert'
import { value as rValue } from '../../parser/lexical' import { value as rValue } from '../../parser/lexical'
import { evalValue } from '../../render/syntax' import { evalValue } from '../../render/syntax'
import BlockMode from '../../scope/block-mode' import BlockMode from '../../context/block-mode'
import TagToken from '../../parser/tag-token' import TagToken from '../../parser/tag-token'
import Token from '../../parser/token' import Token from '../../parser/token'
import Scope from '../../scope/scope' import Context from '../../context/context'
import Hash from '../../template/tag/hash' import Hash from '../../template/tag/hash'
import ITagImplOptions from '../../template/tag/itag-impl-options' import ITagImplOptions from '../../template/tag/itag-impl-options'
@@ -24,23 +24,23 @@ export default {
this.tpls = this.liquid.parser.parse(remainTokens) this.tpls = this.liquid.parser.parse(remainTokens)
}, },
render: async function (scope: Scope, hash: Hash) { render: async function (ctx: Context, hash: Hash) {
const layout = scope.opts.dynamicPartials const layout = ctx.opts.dynamicPartials
? await evalValue(this.layout, scope) ? await evalValue(this.layout, ctx)
: this.staticLayout : this.staticLayout
assert(layout, `cannot apply layout with empty filename`) assert(layout, `cannot apply layout with empty filename`)
// render the remaining tokens immediately // render the remaining tokens immediately
scope.blockMode = BlockMode.STORE ctx.blockMode = BlockMode.STORE
const html = await this.liquid.renderer.renderTemplates(this.tpls, scope) const html = await this.liquid.renderer.renderTemplates(this.tpls, ctx)
if (scope.blocks[''] === undefined) { if (ctx.blocks[''] === undefined) {
scope.blocks[''] = html ctx.blocks[''] = html
} }
const templates = await this.liquid.getTemplate(layout, scope.opts) const templates = await this.liquid.getTemplate(layout, ctx.opts)
scope.push(hash) ctx.push(hash)
scope.blockMode = BlockMode.OUTPUT ctx.blockMode = BlockMode.OUTPUT
const partial = await this.liquid.renderer.renderTemplates(templates, scope) const partial = await this.liquid.renderer.renderTemplates(templates, ctx)
scope.pop(hash) ctx.pop(hash)
return partial return partial
} }
} as ITagImplOptions } as ITagImplOptions
+8 -8
View File
@@ -4,7 +4,7 @@ import { identifier, value, hash } from '../../parser/lexical'
import TagToken from '../../parser/tag-token' import TagToken from '../../parser/tag-token'
import Token from '../../parser/token' import Token from '../../parser/token'
import ITemplate from '../../template/itemplate' import ITemplate from '../../template/itemplate'
import Scope from '../../scope/scope' import Context from '../../context/context'
import Hash from '../../template/tag/hash' import Hash from '../../template/tag/hash'
import ITagImplOptions from '../../template/tag/itag-impl-options' import ITagImplOptions from '../../template/tag/itag-impl-options'
import ParseStream from '../../parser/parse-stream' import ParseStream from '../../parser/parse-stream'
@@ -35,8 +35,8 @@ export default {
stream.start() stream.start()
}, },
render: async function (scope: Scope, hash: Hash) { render: async function (ctx: Context, hash: Hash) {
let collection = await evalExp(this.collection, scope) || [] let collection = await evalExp(this.collection, ctx) || []
const offset = hash.offset || 0 const offset = hash.offset || 0
const limit = (hash.limit === undefined) ? collection.length : hash.limit const limit = (hash.limit === undefined) ? collection.length : hash.limit
@@ -44,22 +44,22 @@ export default {
const cols = hash.cols || collection.length const cols = hash.cols || collection.length
const tablerowloop = new TablerowloopDrop(collection.length, cols) const tablerowloop = new TablerowloopDrop(collection.length, cols)
const ctx = { tablerowloop } const scope = { tablerowloop }
scope.push(ctx) ctx.push(scope)
let html = '' let html = ''
for (let idx = 0; idx < collection.length; idx++, tablerowloop.next()) { for (let idx = 0; idx < collection.length; idx++, tablerowloop.next()) {
ctx[this.variable] = collection[idx] scope[this.variable] = collection[idx]
if (tablerowloop.col0() === 0) { if (tablerowloop.col0() === 0) {
if (tablerowloop.row() !== 1) html += '</tr>' if (tablerowloop.row() !== 1) html += '</tr>'
html += `<tr class="row${tablerowloop.row()}">` html += `<tr class="row${tablerowloop.row()}">`
} }
html += `<td class="col${tablerowloop.col()}">` html += `<td class="col${tablerowloop.col()}">`
html += await this.liquid.renderer.renderTemplates(this.templates, scope) html += await this.liquid.renderer.renderTemplates(this.templates, ctx)
html += '</td>' html += '</td>'
} }
if (collection.length) html += '</tr>' if (collection.length) html += '</tr>'
scope.pop(ctx) ctx.pop(scope)
return html return html
} }
} as ITagImplOptions } as ITagImplOptions
+5 -5
View File
@@ -1,7 +1,7 @@
import { evalExp, isFalsy } from '../../render/syntax' import { evalExp, isFalsy } from '../../render/syntax'
import TagToken from '../../parser/tag-token' import TagToken from '../../parser/tag-token'
import Token from '../../parser/token' import Token from '../../parser/token'
import Scope from '../../scope/scope' import Context from '../../context/context'
import ITagImplOptions from '../../template/tag/itag-impl-options' import ITagImplOptions from '../../template/tag/itag-impl-options'
import ParseStream from '../../parser/parse-stream' import ParseStream from '../../parser/parse-stream'
@@ -25,10 +25,10 @@ export default {
stream.start() stream.start()
}, },
render: async function (scope: Scope) { render: async function (ctx: Context) {
const cond = await evalExp(this.cond, scope) const cond = await evalExp(this.cond, ctx)
return isFalsy(cond) return isFalsy(cond)
? this.liquid.renderer.renderTemplates(this.templates, scope) ? this.liquid.renderer.renderTemplates(this.templates, ctx)
: this.liquid.renderer.renderTemplates(this.elseTemplates, scope) : this.liquid.renderer.renderTemplates(this.elseTemplates, ctx)
} }
} as ITagImplOptions } as ITagImplOptions
+13 -13
View File
@@ -4,12 +4,12 @@ import { __assign } from 'tslib'
import assert from '../util/assert' import assert from '../util/assert'
import { NormalizedFullOptions, applyDefault } from '../liquid-options' import { NormalizedFullOptions, applyDefault } from '../liquid-options'
import BlockMode from './block-mode' import BlockMode from './block-mode'
import { Context } from './context' import { Scope } from './scope'
export default class Scope { export default class Context {
opts: NormalizedFullOptions opts: NormalizedFullOptions
contexts: Array<Context> = [{}] scopes: Array<Scope> = [{}]
environments: Context environments: Scope
blocks: object = {} blocks: object = {}
groups: {[key: string]: number} = {} groups: {[key: string]: number} = {}
blockMode: BlockMode = BlockMode.OUTPUT blockMode: BlockMode = BlockMode.OUTPUT
@@ -18,7 +18,7 @@ export default class Scope {
this.environments = ctx this.environments = ctx
} }
getAll () { getAll () {
return [this.environments, ...this.contexts] return [this.environments, ...this.scopes]
.reduce((ctx, val) => __assign(ctx, val), {}) .reduce((ctx, val) => __assign(ctx, val), {})
} }
async get (path: string) { async get (path: string) {
@@ -33,28 +33,28 @@ export default class Scope {
return ctx return ctx
} }
push (ctx: object) { push (ctx: object) {
return this.contexts.push(ctx) return this.scopes.push(ctx)
} }
pop (ctx?: object): object | undefined { pop (ctx?: object): object | undefined {
if (!arguments.length) { if (!arguments.length) {
return this.contexts.pop() return this.scopes.pop()
} }
const i = this.contexts.findIndex(scope => scope === ctx) const i = this.scopes.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.contexts.splice(i, 1)[0] return this.scopes.splice(i, 1)[0]
} }
findContextFor (key: string) { findContextFor (key: string) {
for (let i = this.contexts.length - 1; i >= 0; i--) { for (let i = this.scopes.length - 1; i >= 0; i--) {
const candidate = this.contexts[i] const candidate = this.scopes[i]
if (key in candidate) { if (key in candidate) {
return candidate return candidate
} }
} }
return null return null
} }
private readProperty (obj: Context, key: string) { private readProperty (obj: Scope, key: string) {
if (_.isNil(obj)) return obj if (_.isNil(obj)) return obj
obj = _.toLiquid(obj) obj = _.toLiquid(obj)
if (obj instanceof Drop) { if (obj instanceof Drop) {
@@ -125,7 +125,7 @@ export default class Scope {
} }
} }
function readSize (obj: Context) { function readSize (obj: Scope) {
if (!_.isNil(obj['size'])) return obj['size'] if (!_.isNil(obj['size'])) return obj['size']
if (_.isArray(obj) || _.isString(obj)) return obj.length if (_.isArray(obj) || _.isString(obj)) return obj.length
return obj['size'] return obj['size']
@@ -5,4 +5,4 @@ type PlainObject = {
toLiquid?: () => any toLiquid?: () => any
} }
export type Context = PlainObject | Drop export type Scope = PlainObject | Drop
+4 -4
View File
@@ -1,4 +1,4 @@
import Scope from './scope/scope' import Context from './context/context'
import * as Types from './types' import * as Types from './types'
import fs from './fs/node' import fs from './fs/node'
import * as _ from './util/underscore' import * as _ from './util/underscore'
@@ -38,7 +38,7 @@ export default class Liquid {
} }
render (tpl: Array<ITemplate>, ctx?: object, opts?: LiquidOptions) { render (tpl: Array<ITemplate>, ctx?: object, opts?: LiquidOptions) {
const options = { ...this.options, ...normalize(opts) } const options = { ...this.options, ...normalize(opts) }
const scope = new Scope(ctx, options) const scope = new Context(ctx, options)
return this.renderer.renderTemplates(tpl, scope) return this.renderer.renderTemplates(tpl, scope)
} }
async parseAndRender (html: string, ctx?: object, opts?: LiquidOptions) { async parseAndRender (html: string, ctx?: object, opts?: LiquidOptions) {
@@ -69,8 +69,8 @@ export default class Liquid {
const templates = await this.getTemplate(file, options) const templates = await this.getTemplate(file, options)
return this.render(templates, ctx, opts) return this.render(templates, ctx, opts)
} }
evalValue (str: string, scope: Scope) { evalValue (str: string, ctx: Context) {
return new Value(str, this.options.strictFilters).value(scope) return new Value(str, this.options.strictFilters).value(ctx)
} }
registerFilter (name: string, filter: FilterImpl) { registerFilter (name: string, filter: FilterImpl) {
return Filter.register(name, filter) return Filter.register(name, filter)
+4 -4
View File
@@ -1,16 +1,16 @@
import { RenderError } from '../util/error' import { RenderError } from '../util/error'
import assert from '../util/assert' import assert from '../util/assert'
import Scope from '../scope/scope' import Context from '../context/context'
import ITemplate from '../template/itemplate' import ITemplate from '../template/itemplate'
export default class Render { export default class Render {
async renderTemplates (templates: ITemplate[], scope: Scope) { async renderTemplates (templates: ITemplate[], ctx: Context) {
assert(scope, 'unable to evalTemplates: scope undefined') assert(ctx, 'unable to evalTemplates: context undefined')
let html = '' let html = ''
for (const tpl of templates) { for (const tpl of templates) {
try { try {
html += await tpl.render(scope) html += await tpl.render(ctx)
} catch (e) { } catch (e) {
if (e.name === 'RenderBreakError') { if (e.name === 'RenderBreakError') {
e.resolvedHTML = html e.resolvedHTML = html
+14 -14
View File
@@ -1,6 +1,6 @@
import * as lexical from '../parser/lexical' import * as lexical from '../parser/lexical'
import assert from '../util/assert' import assert from '../util/assert'
import Scope from '../scope/scope' import Context from '../context/context'
import { range, last } from '../util/underscore' import { range, last } from '../util/underscore'
import { isComparable } from '../drop/icomparable' import { isComparable } from '../drop/icomparable'
import { NullDrop } from '../drop/null-drop' import { NullDrop } from '../drop/null-drop'
@@ -48,36 +48,36 @@ const binaryOperators: {[key: string]: (lhs: any, rhs: any) => boolean} = {
'or': (l: any, r: any) => isTruthy(l) || isTruthy(r) 'or': (l: any, r: any) => isTruthy(l) || isTruthy(r)
} }
export async function parseExp (exp: string, scope: Scope): Promise<any> { export async function parseExp (exp: string, ctx: Context): Promise<any> {
assert(scope, 'unable to parseExp: scope undefined') assert(ctx, 'unable to parseExp: scope undefined')
const operatorREs = lexical.operators const operatorREs = lexical.operators
let match let match
for (let i = 0; i < operatorREs.length; i++) { for (let i = 0; i < operatorREs.length; i++) {
const operatorRE = operatorREs[i] const operatorRE = operatorREs[i]
const expRE = new RegExp(`^(${lexical.quoteBalanced.source})(${operatorRE.source})(${lexical.quoteBalanced.source})$`) const expRE = new RegExp(`^(${lexical.quoteBalanced.source})(${operatorRE.source})(${lexical.quoteBalanced.source})$`)
if ((match = exp.match(expRE))) { if ((match = exp.match(expRE))) {
const l = await parseExp(match[1], scope) const l = await parseExp(match[1], ctx)
const op = binaryOperators[match[2].trim()] const op = binaryOperators[match[2].trim()]
const r = await parseExp(match[3], scope) const r = await parseExp(match[3], ctx)
return op(l, r) return op(l, r)
} }
} }
if ((match = exp.match(lexical.rangeLine))) { if ((match = exp.match(lexical.rangeLine))) {
const low = await evalValue(match[1], scope) const low = await evalValue(match[1], ctx)
const high = await evalValue(match[2], scope) const high = await evalValue(match[2], ctx)
return range(+low, +high + 1) return range(+low, +high + 1)
} }
return parseValue(exp, scope) return parseValue(exp, ctx)
} }
export async function evalExp (str: string, scope: Scope): Promise<any> { export async function evalExp (str: string, ctx: Context): Promise<any> {
const value = await parseExp(str, scope) const value = await parseExp(str, ctx)
return value instanceof Drop ? value.valueOf() : value return value instanceof Drop ? value.valueOf() : value
} }
async function parseValue (str: string | undefined, scope: Scope): Promise<any> { async function parseValue (str: string | undefined, ctx: Context): Promise<any> {
if (!str) return null if (!str) return null
str = str.trim() str = str.trim()
@@ -88,11 +88,11 @@ async function parseValue (str: string | undefined, scope: Scope): Promise<any>
if (str === 'blank') return new BlankDrop() if (str === 'blank') return new BlankDrop()
if (!isNaN(Number(str))) return Number(str) if (!isNaN(Number(str))) return Number(str)
if ((str[0] === '"' || str[0] === "'") && str[0] === last(str)) return str.slice(1, -1) if ((str[0] === '"' || str[0] === "'") && str[0] === last(str)) return str.slice(1, -1)
return scope.get(str) return ctx.get(str)
} }
export async function evalValue (str: string | undefined, scope: Scope) { export async function evalValue (str: string | undefined, ctx: Context) {
const value = await parseValue(str, scope) const value = await parseValue(str, ctx)
return value instanceof Drop ? value.valueOf() : value return value instanceof Drop ? value.valueOf() : value
} }
+4 -4
View File
@@ -1,5 +1,5 @@
import { evalValue } from '../../render/syntax' import { evalValue } from '../../render/syntax'
import Scope from '../../scope/scope' import Context from '../../context/context'
import { isArray } from '../../util/underscore' import { isArray } from '../../util/underscore'
import { FilterImpl } from './filter-impl' import { FilterImpl } from './filter-impl'
@@ -19,11 +19,11 @@ export class Filter {
this.impl = impl || (x => x) this.impl = impl || (x => x)
this.args = args this.args = args
} }
async render (value: any, scope: Scope) { async render (value: any, ctx: Context) {
const argv: any[] = [] const argv: any[] = []
for (const arg of this.args) { for (const arg of this.args) {
if (isArray(arg)) argv.push([arg[0], await evalValue(arg[1], scope)]) if (isArray(arg)) argv.push([arg[0], await evalValue(arg[1], ctx)])
else argv.push(await evalValue(arg, scope)) else argv.push(await evalValue(arg, ctx))
} }
return this.impl.apply(null, [value, ...argv]) return this.impl.apply(null, [value, ...argv])
} }
+2 -2
View File
@@ -1,7 +1,7 @@
import Scope from '../scope/scope' import Context from '../context/context'
import Token from '../parser/token' import Token from '../parser/token'
export default interface ITemplate { export default interface ITemplate {
token: Token; token: Token;
render(scope: Scope): Promise<string>; render(ctx: Context): Promise<string>;
} }
+3 -3
View File
@@ -2,7 +2,7 @@ import Value from './value'
import { stringify } from '../util/underscore' import { stringify } from '../util/underscore'
import Template from '../template/template' import Template from '../template/template'
import ITemplate from '../template/itemplate' import ITemplate from '../template/itemplate'
import Scope from '../scope/scope' import Context from '../context/context'
import OutputToken from '../parser/output-token' import OutputToken from '../parser/output-token'
export default class Output extends Template<OutputToken> implements ITemplate { export default class Output extends Template<OutputToken> implements ITemplate {
@@ -11,8 +11,8 @@ export default class Output extends Template<OutputToken> implements ITemplate {
super(token) super(token)
this.value = new Value(token.value, strictFilters) this.value = new Value(token.value, strictFilters)
} }
async render (scope: Scope): Promise<string> { async render (ctx: Context): Promise<string> {
const html = await this.value.value(scope) const html = await this.value.value(ctx)
return stringify(html) return stringify(html)
} }
} }
+3 -3
View File
@@ -1,6 +1,6 @@
import { hashCapture } from '../../parser/lexical' import { hashCapture } from '../../parser/lexical'
import { evalValue } from '../../render/syntax' import { evalValue } from '../../render/syntax'
import Scope from '../../scope/scope' import Context from '../../context/context'
/** /**
* Key-Value Pairs Representing Tag Arguments * Key-Value Pairs Representing Tag Arguments
@@ -10,14 +10,14 @@ import Scope from '../../scope/scope'
*/ */
export default class Hash { export default class Hash {
[key: string]: any [key: string]: any
static async create (markup: string, scope: Scope) { static async create (markup: string, ctx: Context) {
const instance = new Hash() const instance = new Hash()
let match let match
hashCapture.lastIndex = 0 hashCapture.lastIndex = 0
while ((match = hashCapture.exec(markup))) { while ((match = hashCapture.exec(markup))) {
const k = match[1] const k = match[1]
const v = match[2] const v = match[2]
instance[k] = await evalValue(v, scope) instance[k] = await evalValue(v, ctx)
} }
return instance return instance
} }
+2 -2
View File
@@ -1,4 +1,4 @@
import Scope from '../../scope/scope' import Context from '../../context/context'
import TagToken from '../../parser/tag-token' import TagToken from '../../parser/tag-token'
import Token from '../../parser/token' import Token from '../../parser/token'
import Hash from '../../template/tag/hash' import Hash from '../../template/tag/hash'
@@ -6,5 +6,5 @@ import ITagImpl from './itag-impl'
export default interface ITagImplOptions { export default interface ITagImplOptions {
parse?: (this: ITagImpl, token: TagToken, remainingTokens: Array<Token>) => void parse?: (this: ITagImpl, token: TagToken, remainingTokens: Array<Token>) => void
render?: (this: ITagImpl, scope: Scope, hash: Hash) => any | Promise<any> render?: (this: ITagImpl, ctx: Context, hash: Hash) => any | Promise<any>
} }
+4 -4
View File
@@ -1,6 +1,6 @@
import { create, stringify } from '../../util/underscore' import { create, stringify } from '../../util/underscore'
import assert from '../../util/assert' import assert from '../../util/assert'
import Scope from '../../scope/scope' import Context from '../../context/context'
import ITagImpl from './itag-impl' import ITagImpl from './itag-impl'
import ITagImplOptions from './itag-impl-options' import ITagImplOptions from './itag-impl-options'
import Liquid from '../../liquid' import Liquid from '../../liquid'
@@ -27,13 +27,13 @@ export default class Tag extends Template<TagToken> implements ITemplate {
this.impl.parse(token, tokens) this.impl.parse(token, tokens)
} }
} }
async render (scope: Scope) { async render (ctx: Context) {
const hash = await Hash.create(this.token.args, scope) const hash = await Hash.create(this.token.args, ctx)
const impl = this.impl const impl = this.impl
if (typeof impl.render !== 'function') { if (typeof impl.render !== 'function') {
return '' return ''
} }
const html = await impl.render(scope, hash) const html = await impl.render(ctx, hash)
return stringify(html) return stringify(html)
} }
static register (name: string, tag: ITagImplOptions) { static register (name: string, tag: ITagImplOptions) {
+4 -4
View File
@@ -1,6 +1,6 @@
import { evalExp } from '../render/syntax' import { evalExp } from '../render/syntax'
import { FilterArgs, Filter } from './filter/filter' import { FilterArgs, Filter } from './filter/filter'
import Scope from '../scope/scope' import Context from '../context/context'
export default class Value { export default class Value {
private strictFilters: boolean private strictFilters: boolean
@@ -47,10 +47,10 @@ export default class Value {
} }
this.filters.push(new Filter(name, args, this.strictFilters)) this.filters.push(new Filter(name, args, this.strictFilters))
} }
async value (scope: Scope) { async value (ctx: Context) {
let val = await evalExp(this.initial, scope) let val = await evalExp(this.initial, ctx)
for (const filter of this.filters) { for (const filter of this.filters) {
val = await filter.render(val, scope) val = await filter.render(val, ctx)
} }
return val return val
} }
+2 -2
View File
@@ -1,12 +1,12 @@
import Liquid from '../../../../src/liquid' import Liquid from '../../../../src/liquid'
import { expect, use } from 'chai' import { expect, use } from 'chai'
import * as chaiAsPromised from 'chai-as-promised' import * as chaiAsPromised from 'chai-as-promised'
import { Context } from '../../../../src/scope/context' import { Scope } from '../../../../src/context/scope'
use(chaiAsPromised) use(chaiAsPromised)
describe('tags/for', function () { describe('tags/for', function () {
let liquid: Liquid, ctx: Context let liquid: Liquid, ctx: Scope
before(function () { before(function () {
liquid = new Liquid() liquid = new Liquid()
liquid.registerTag('throwingTag', { liquid.registerTag('throwingTag', {
@@ -1,13 +1,13 @@
import * as chai from 'chai' import * as chai from 'chai'
import Scope from '../../../src/scope/scope' import Context from '../../../src/context/context'
import { Context } from '../../../src/scope/context' import { Scope } from '../../../src/context/scope'
const expect = chai.expect const expect = chai.expect
describe('scope', function () { describe('scope', function () {
let scope: Scope, ctx: Context let ctx: Context, scope: Scope
beforeEach(function () { beforeEach(function () {
ctx = { scope = {
foo: 'zoo', foo: 'zoo',
one: 1, one: 1,
zoo: { size: 4 }, zoo: { size: 4 },
@@ -17,76 +17,76 @@ describe('scope', function () {
arr: ['a', 'b'] arr: ['a', 'b']
} }
} }
scope = new Scope(ctx) ctx = new Context(scope)
}) })
describe('#propertyAccessSeq()', function () { describe('#propertyAccessSeq()', function () {
it('should handle dot syntax', async function () { it('should handle dot syntax', async function () {
expect(await scope.propertyAccessSeq('foo.bar')) expect(await ctx.propertyAccessSeq('foo.bar'))
.to.deep.equal(['foo', 'bar']) .to.deep.equal(['foo', 'bar'])
}) })
it('should handle [<String>] syntax', async function () { it('should handle [<String>] syntax', async function () {
expect(await scope.propertyAccessSeq('foo["bar"]')) expect(await ctx.propertyAccessSeq('foo["bar"]'))
.to.deep.equal(['foo', 'bar']) .to.deep.equal(['foo', 'bar'])
}) })
it('should handle [<Identifier>] syntax', async function () { it('should handle [<Identifier>] syntax', async function () {
expect(await scope.propertyAccessSeq('foo[foo]')) expect(await ctx.propertyAccessSeq('foo[foo]'))
.to.deep.equal(['foo', 'zoo']) .to.deep.equal(['foo', 'zoo'])
}) })
it('should handle nested access 1', async function () { it('should handle nested access 1', async function () {
expect(await scope.propertyAccessSeq('foo[bar.zoo]')) expect(await ctx.propertyAccessSeq('foo[bar.zoo]'))
.to.deep.equal(['foo', 'coo']) .to.deep.equal(['foo', 'coo'])
}) })
it('should handle nested access 2', async function () { it('should handle nested access 2', async function () {
expect(await scope.propertyAccessSeq('foo[bar["zoo"]]')) expect(await ctx.propertyAccessSeq('foo[bar["zoo"]]'))
.to.deep.equal(['foo', 'coo']) .to.deep.equal(['foo', 'coo'])
}) })
it('should handle nested access 3', async function () { it('should handle nested access 3', async function () {
expect(await scope.propertyAccessSeq('bar["foo"].zoo')) expect(await ctx.propertyAccessSeq('bar["foo"].zoo'))
.to.deep.equal(['bar', 'foo', 'zoo']) .to.deep.equal(['bar', 'foo', 'zoo'])
}) })
it('should handle nested access 4', async function () { it('should handle nested access 4', async function () {
expect(await scope.propertyAccessSeq('foo[0].bar')) expect(await ctx.propertyAccessSeq('foo[0].bar'))
.to.deep.equal(['foo', '0', 'bar']) .to.deep.equal(['foo', '0', 'bar'])
}) })
it('should handle nested access 5', async function () { it('should handle nested access 5', async function () {
expect(await scope.propertyAccessSeq('foo[one].bar')) expect(await ctx.propertyAccessSeq('foo[one].bar'))
.to.deep.equal(['foo', '1', 'bar']) .to.deep.equal(['foo', '1', 'bar'])
}) })
it('should handle nested access 6', async function () { it('should handle nested access 6', async function () {
expect(await scope.propertyAccessSeq('foo[two].bar')) expect(await ctx.propertyAccessSeq('foo[two].bar'))
.to.deep.equal(['foo', 'undefined', 'bar']) .to.deep.equal(['foo', 'undefined', 'bar'])
}) })
}) })
describe('#get()', function () { describe('#get()', function () {
it('should get direct property', async function () { it('should get direct property', async function () {
expect(await await scope.get('foo')).equal('zoo') expect(await await ctx.get('foo')).equal('zoo')
}) })
it('undefined property should yield undefined', async function () { it('undefined property should yield undefined', async function () {
expect(scope.get('notdefined')).to.be.rejected expect(ctx.get('notdefined')).to.be.rejected
expect(await scope.get('notdefined')).to.equal(undefined) expect(await ctx.get('notdefined')).to.equal(undefined)
expect(await scope.get(false as any)).to.equal(undefined) expect(await ctx.get(false as any)).to.equal(undefined)
}) })
it('should throw for invalid path', async function () { it('should throw for invalid path', async function () {
expect(scope.get('')).to.be.rejectedWith('invalid path:""') expect(ctx.get('')).to.be.rejectedWith('invalid path:""')
}) })
it('should throw when [] unbalanced', async function () { it('should throw when [] unbalanced', async function () {
expect(scope.get('foo[bar')).to.be.rejectedWith(/unbalanced \[\]/) expect(ctx.get('foo[bar')).to.be.rejectedWith(/unbalanced \[\]/)
}) })
it('should throw when "" unbalanced', async function () { it('should throw when "" unbalanced', async function () {
expect(scope.get('foo["bar]')).to.be.rejectedWith(/unbalanced "/) expect(ctx.get('foo["bar]')).to.be.rejectedWith(/unbalanced "/)
}) })
it("should throw when '' unbalanced", async function () { it("should throw when '' unbalanced", async function () {
expect(scope.get("foo['bar]")).to.be.rejectedWith(/unbalanced '/) expect(ctx.get("foo['bar]")).to.be.rejectedWith(/unbalanced '/)
}) })
it('should respect to toLiquid', async function () { it('should respect to toLiquid', async function () {
const scope = new Scope({ foo: { const scope = new Context({ foo: {
toLiquid: () => ({ bar: 'BAR' }), toLiquid: () => ({ bar: 'BAR' }),
bar: 'bar' bar: 'bar'
} }) } })
@@ -94,94 +94,94 @@ describe('scope', function () {
}) })
it('should access child property via dot syntax', async function () { it('should access child property via dot syntax', async function () {
expect(await scope.get('bar.zoo')).to.equal('coo') expect(await ctx.get('bar.zoo')).to.equal('coo')
expect(await scope.get('bar.arr')).to.deep.equal(['a', 'b']) expect(await ctx.get('bar.arr')).to.deep.equal(['a', 'b'])
}) })
it('should access child property via [<String>] syntax', async function () { it('should access child property via [<String>] syntax', async function () {
expect(await scope.get('bar["zoo"]')).to.equal('coo') expect(await ctx.get('bar["zoo"]')).to.equal('coo')
}) })
it('should access child property via [<Number>] syntax', async function () { it('should access child property via [<Number>] syntax', async function () {
expect(await scope.get('bar.arr[0]')).to.equal('a') expect(await ctx.get('bar.arr[0]')).to.equal('a')
}) })
it('should access child property via [<Identifier>] syntax', async function () { it('should access child property via [<Identifier>] syntax', async function () {
expect(await scope.get('bar[foo]')).to.equal('coo') expect(await ctx.get('bar[foo]')).to.equal('coo')
}) })
it('should return undefined when not exist', async function () { it('should return undefined when not exist', async function () {
expect(await scope.get('foo.foo.foo')).to.be.undefined expect(await ctx.get('foo.foo.foo')).to.be.undefined
}) })
it('should return string length as size', async function () { it('should return string length as size', async function () {
expect(await scope.get('foo.size')).to.equal(3) expect(await ctx.get('foo.size')).to.equal(3)
}) })
it('should return array length as size', async function () { it('should return array length as size', async function () {
expect(await scope.get('bar.arr.size')).to.equal(2) expect(await ctx.get('bar.arr.size')).to.equal(2)
}) })
it('should return size property if exists', async function () { it('should return size property if exists', async function () {
expect(await scope.get('zoo.size')).to.equal(4) expect(await ctx.get('zoo.size')).to.equal(4)
}) })
it('should return undefined if do not have size and length', async function () { it('should return undefined if do not have size and length', async function () {
expect(await scope.get('one.size')).to.equal(undefined) expect(await ctx.get('one.size')).to.equal(undefined)
}) })
}) })
describe('strictVariables', async function () { describe('strictVariables', async function () {
let scope: Scope let ctx: Context
beforeEach(function () { beforeEach(function () {
scope = new Scope(ctx, { ctx = new Context(ctx, {
strictVariables: true strictVariables: true
} as any) } as any)
}) })
it('should throw when variable not defined', function () { it('should throw when variable not defined', function () {
return expect(scope.get('notdefined')).to.be.rejectedWith(/undefined variable: notdefined/) return expect(ctx.get('notdefined')).to.be.rejectedWith(/undefined variable: notdefined/)
}) })
it('should throw when deep variable not exist', async function () { it('should throw when deep variable not exist', async function () {
scope.contexts.push({ 'foo': 'FOO' }) ctx.scopes.push({ 'foo': 'FOO' })
return expect(scope.get('foo.bar.not.defined')).to.be.rejectedWith(/undefined variable: bar/) return expect(ctx.get('foo.bar.not.defined')).to.be.rejectedWith(/undefined variable: bar/)
}) })
it('should throw when itself not defined', async function () { it('should throw when itself not defined', async function () {
scope.contexts.push({ 'foo': 'FOO' }) ctx.scopes.push({ 'foo': 'FOO' })
return expect(scope.get('foo.BAR')).to.be.rejectedWith(/undefined variable: BAR/) return expect(ctx.get('foo.BAR')).to.be.rejectedWith(/undefined variable: BAR/)
}) })
it('should find variable in parent scope', async function () { it('should find variable in parent scope', async function () {
scope.contexts.push({ 'foo': 'foo' }) ctx.scopes.push({ 'foo': 'foo' })
scope.push({ ctx.push({
'bar': 'bar' 'bar': 'bar'
}) })
expect(await scope.get('foo')).to.equal('foo') expect(await ctx.get('foo')).to.equal('foo')
}) })
}) })
describe('.getAll()', function () { describe('.getAll()', function () {
it('should get all properties when arguments empty', async function () { it('should get all properties when arguments empty', async function () {
expect(await scope.getAll()).deep.equal(ctx) expect(await ctx.getAll()).deep.equal(scope)
}) })
}) })
describe('.push()', function () { describe('.push()', function () {
it('should push scope', async function () { it('should push scope', async function () {
scope.contexts.push({ 'bar': 'bar' }) ctx.scopes.push({ 'bar': 'bar' })
scope.push({ ctx.push({
foo: 'foo' foo: 'foo'
}) })
expect(await scope.get('foo')).to.equal('foo') expect(await ctx.get('foo')).to.equal('foo')
expect(await scope.get('bar')).to.equal('bar') expect(await ctx.get('bar')).to.equal('bar')
}) })
it('should hide deep properties by push', async function () { it('should hide deep properties by push', async function () {
scope.contexts.push({ 'bar': { bar: 'bar' } }) ctx.scopes.push({ 'bar': { bar: 'bar' } })
scope.push({ bar: { foo: 'foo' } }) ctx.push({ bar: { foo: 'foo' } })
expect(await scope.get('bar.foo')).to.equal('foo') expect(await ctx.get('bar.foo')).to.equal('foo')
expect(await scope.get('bar.bar')).to.equal(undefined) expect(await ctx.get('bar.bar')).to.equal(undefined)
}) })
}) })
describe('.pop()', function () { describe('.pop()', function () {
it('should pop scope', async function () { it('should pop scope', async function () {
scope.push({ ctx.push({
foo: 'foo' foo: 'foo'
}) })
scope.pop() ctx.pop()
expect(await scope.get('foo')).to.equal('zoo') expect(await ctx.get('foo')).to.equal('zoo')
}) })
}) })
it('should pop specified scope', async function () { it('should pop specified scope', async function () {
@@ -191,18 +191,18 @@ describe('scope', function () {
const scope2 = { const scope2 = {
bar: 'bar' bar: 'bar'
} }
scope.push(scope1) ctx.push(scope1)
scope.push(scope2) ctx.push(scope2)
expect(await scope.get('foo')).to.equal('foo') expect(await ctx.get('foo')).to.equal('foo')
expect(await scope.get('bar')).to.equal('bar') expect(await ctx.get('bar')).to.equal('bar')
scope.pop(scope1) ctx.pop(scope1)
expect(await scope.get('foo')).to.equal('zoo') expect(await ctx.get('foo')).to.equal('zoo')
expect(await scope.get('bar')).to.equal('bar') expect(await ctx.get('bar')).to.equal('bar')
}) })
it('should throw when specified scope not found', function () { it('should throw when specified scope not found', function () {
const scope1 = { const scope1 = {
foo: 'foo' foo: 'foo'
} }
expect(() => scope.pop(scope1)).to.throw('scope not found, cannot pop') expect(() => ctx.pop(scope1)).to.throw('scope not found, cannot pop')
}) })
}) })
+2 -2
View File
@@ -1,5 +1,5 @@
import { expect } from 'chai' import { expect } from 'chai'
import Scope from '../../../src/scope/scope' import Context from '../../../src/context/context'
import Token from '../../../src/parser/token' import Token from '../../../src/parser/token'
import Tag from '../../../src/template/tag/tag' import Tag from '../../../src/template/tag/tag'
import { Filter } from '../../../src/template/filter/filter' import { Filter } from '../../../src/template/filter/filter'
@@ -20,7 +20,7 @@ describe('render', function () {
}) })
it('should render html', async function () { it('should render html', async function () {
const scope = new Scope() const scope = new Context()
const token = { type: 'html', value: '<p>' } as Token const token = { type: 'html', value: '<p>' } as Token
const html = await render.renderTemplates([new HTML(token)], scope) const html = await render.renderTemplates([new HTML(token)], scope)
return expect(html).to.equal('<p>') return expect(html).to.equal('<p>')
+34 -34
View File
@@ -1,12 +1,12 @@
import Scope from '../../../src/scope/scope' import Context from '../../../src/context/context'
import { expect } from 'chai' import { expect } from 'chai'
import { evalExp, evalValue, isTruthy } from '../../../src/render/syntax' import { evalExp, evalValue, isTruthy } from '../../../src/render/syntax'
describe('render/syntax', function () { describe('render/syntax', function () {
let scope: Scope let ctx: Context
beforeEach(function () { beforeEach(function () {
scope = new Scope({ ctx = new Context({
one: 1, one: 1,
two: 2, two: 2,
empty: '', empty: '',
@@ -19,30 +19,30 @@ describe('render/syntax', function () {
describe('.evalValue()', function () { describe('.evalValue()', function () {
it('should eval boolean literal', async function () { it('should eval boolean literal', async function () {
expect(await evalValue('true', scope)).to.equal(true) expect(await evalValue('true', ctx)).to.equal(true)
expect(await evalValue('TrUE', scope)).to.equal(undefined) expect(await evalValue('TrUE', ctx)).to.equal(undefined)
expect(await evalValue('false', scope)).to.equal(false) expect(await evalValue('false', ctx)).to.equal(false)
}) })
it('should eval number literal', async function () { it('should eval number literal', async function () {
expect(await evalValue('2.3', scope)).to.equal(2.3) expect(await evalValue('2.3', ctx)).to.equal(2.3)
expect(await evalValue('.32', scope)).to.equal(0.32) expect(await evalValue('.32', ctx)).to.equal(0.32)
expect(await evalValue('-23.', scope)).to.equal(-23) expect(await evalValue('-23.', ctx)).to.equal(-23)
expect(await evalValue('23', scope)).to.equal(23) expect(await evalValue('23', ctx)).to.equal(23)
}) })
it('should eval string literal', async function () { it('should eval string literal', async function () {
expect(await evalValue('"ab\'c"', scope)).to.equal("ab'c") expect(await evalValue('"ab\'c"', ctx)).to.equal("ab'c")
expect(await evalValue("'ab\"c'", scope)).to.equal('ab"c') expect(await evalValue("'ab\"c'", ctx)).to.equal('ab"c')
}) })
it('should eval nil literal', async function () { it('should eval nil literal', async function () {
expect(await evalValue('nil', scope)).to.be.null expect(await evalValue('nil', ctx)).to.be.null
}) })
it('should eval null literal', async function () { it('should eval null literal', async function () {
expect(await evalValue('null', scope)).to.be.null expect(await evalValue('null', ctx)).to.be.null
}) })
it('should eval scope variables', async function () { it('should eval scope variables', async function () {
expect(await evalValue('one', scope)).to.equal(1) expect(await evalValue('one', ctx)).to.equal(1)
expect(await evalValue('has_value?', scope)).to.equal(true) expect(await evalValue('has_value?', ctx)).to.equal(true)
expect(await evalValue('x', scope)).to.equal('XXX') expect(await evalValue('x', ctx)).to.equal('XXX')
}) })
}) })
@@ -66,37 +66,37 @@ describe('render/syntax', function () {
}) })
it('should eval simple expression', async function () { it('should eval simple expression', async function () {
expect(await evalExp('1<2', scope)).to.equal(true) expect(await evalExp('1<2', ctx)).to.equal(true)
expect(await evalExp('2<=2', scope)).to.equal(true) expect(await evalExp('2<=2', ctx)).to.equal(true)
expect(await evalExp('one<=two', scope)).to.equal(true) expect(await evalExp('one<=two', ctx)).to.equal(true)
expect(await evalExp('x contains "x"', scope)).to.equal(false) expect(await evalExp('x contains "x"', ctx)).to.equal(false)
expect(await evalExp('x contains "X"', scope)).to.equal(true) expect(await evalExp('x contains "X"', ctx)).to.equal(true)
expect(await evalExp('1 contains "x"', scope)).to.equal(false) expect(await evalExp('1 contains "x"', ctx)).to.equal(false)
expect(await evalExp('y contains "x"', scope)).to.equal(false) expect(await evalExp('y contains "x"', ctx)).to.equal(false)
expect(await evalExp('z contains "x"', scope)).to.equal(false) expect(await evalExp('z contains "x"', ctx)).to.equal(false)
expect(await evalExp('(1..5) contains 3', scope)).to.equal(true) expect(await evalExp('(1..5) contains 3', ctx)).to.equal(true)
expect(await evalExp('(1..5) contains 6', scope)).to.equal(false) expect(await evalExp('(1..5) contains 6', ctx)).to.equal(false)
expect(await evalExp('"<=" == "<="', scope)).to.equal(true) expect(await evalExp('"<=" == "<="', ctx)).to.equal(true)
}) })
describe('complex expression', function () { describe('complex expression', function () {
it('should support value or value', async function () { it('should support value or value', async function () {
expect(await evalExp('false or true', scope)).to.equal(true) expect(await evalExp('false or true', ctx)).to.equal(true)
}) })
it('should support < and contains', async function () { it('should support < and contains', async function () {
expect(await evalExp('1<2 and x contains "x"', scope)).to.equal(false) expect(await evalExp('1<2 and x contains "x"', ctx)).to.equal(false)
}) })
it('should support < or contains', async function () { it('should support < or contains', async function () {
expect(await evalExp('1<2 or x contains "x"', scope)).to.equal(true) expect(await evalExp('1<2 or x contains "x"', ctx)).to.equal(true)
}) })
it('should support value and !=', async function () { it('should support value and !=', async function () {
expect(await evalExp('empty and empty != ""', scope)).to.equal(false) expect(await evalExp('empty and empty != ""', ctx)).to.equal(false)
}) })
}) })
it('should eval range expression', async function () { it('should eval range expression', async function () {
expect(await evalExp('(2..4)', scope)).to.deep.equal([2, 3, 4]) expect(await evalExp('(2..4)', ctx)).to.deep.equal([2, 3, 4])
expect(await evalExp('(two..4)', scope)).to.deep.equal([2, 3, 4]) expect(await evalExp('(two..4)', ctx)).to.deep.equal([2, 3, 4])
}) })
}) })
}) })
+8 -8
View File
@@ -2,16 +2,16 @@ import * as chai from 'chai'
import * as sinon from 'sinon' import * as sinon from 'sinon'
import * as sinonChai from 'sinon-chai' import * as sinonChai from 'sinon-chai'
import { Filter } from '../../../../src/template/filter/filter' import { Filter } from '../../../../src/template/filter/filter'
import Scope from '../../../../src/scope/scope' import Context from '../../../../src/context/context'
chai.use(sinonChai) chai.use(sinonChai)
const expect = chai.expect const expect = chai.expect
describe('filter', function () { describe('filter', function () {
let scope: Scope let ctx: Context
beforeEach(function () { beforeEach(function () {
Filter.clear() Filter.clear()
scope = new Scope() ctx = new Context()
}) })
it('should create default filter if not registered', async function () { it('should create default filter if not registered', async function () {
const result = new Filter('foo', [], false) const result = new Filter('foo', [], false)
@@ -19,28 +19,28 @@ describe('filter', function () {
}) })
it('should render input if filter not registered', async function () { it('should render input if filter not registered', async function () {
expect(await new Filter('undefined', [], false).render('foo', scope)).to.equal('foo') expect(await new Filter('undefined', [], false).render('foo', ctx)).to.equal('foo')
}) })
it('should call filter impl with corrct arguments', async function () { it('should call filter impl with corrct arguments', async function () {
const spy = sinon.spy() const spy = sinon.spy()
Filter.register('foo', spy) Filter.register('foo', spy)
await new Filter('foo', ['33'], false).render('foo', scope) await new Filter('foo', ['33'], false).render('foo', ctx)
expect(spy).to.have.been.calledWith('foo', 33) expect(spy).to.have.been.calledWith('foo', 33)
}) })
it('should render a simple filter', async function () { it('should render a simple filter', async function () {
Filter.register('upcase', x => x.toUpperCase()) Filter.register('upcase', x => x.toUpperCase())
expect(await new Filter('upcase', [], false).render('foo', scope)).to.equal('FOO') expect(await new Filter('upcase', [], false).render('foo', ctx)).to.equal('FOO')
}) })
it('should render filters with argument', async function () { it('should render filters with argument', async function () {
Filter.register('add', (a, b) => a + b) Filter.register('add', (a, b) => a + b)
expect(await new Filter('add', ['2'], false).render(3, scope)).to.equal(5) expect(await new Filter('add', ['2'], false).render(3, ctx)).to.equal(5)
}) })
it('should render filters with multiple arguments', async function () { it('should render filters with multiple arguments', async function () {
Filter.register('add', (a, b, c) => a + b + c) Filter.register('add', (a, b, c) => a + b + c)
expect(await new Filter('add', ['2', '"c"'], false).render(3, scope)).to.equal('5c') expect(await new Filter('add', ['2', '"c"'], false).render(3, ctx)).to.equal('5c')
}) })
it('should not throw when filter name illegal', function () { it('should not throw when filter name illegal', function () {
+5 -5
View File
@@ -1,5 +1,5 @@
import * as chai from 'chai' import * as chai from 'chai'
import Scope from '../../../src/scope/scope' import Context from '../../../src/context/context'
import Output from '../../../src/template/output' import Output from '../../../src/template/output'
import OutputToken from '../../../src/parser/output-token' import OutputToken from '../../../src/parser/output-token'
import { Filter } from '../../../src/template/filter/filter' import { Filter } from '../../../src/template/filter/filter'
@@ -12,7 +12,7 @@ describe('Output', function () {
}) })
it('should stringify objects', async function () { it('should stringify objects', async function () {
const scope = new Scope({ const scope = new Context({
foo: { obj: { arr: ['a', 2] } } foo: { obj: { arr: ['a', 2] } }
}) })
const output = new Output({ value: 'foo' } as OutputToken, false) const output = new Output({ value: 'foo' } as OutputToken, false)
@@ -20,19 +20,19 @@ describe('Output', function () {
return expect(html).to.equal('[object Object]') return expect(html).to.equal('[object Object]')
}) })
it('should skip function property', async function () { it('should skip function property', async function () {
const scope = new Scope({ obj: { foo: 'foo', bar: (x: any) => x } }) const scope = new Context({ obj: { foo: 'foo', bar: (x: any) => x } })
const output = new Output({ value: 'obj' } as OutputToken, false) const output = new Output({ value: 'obj' } as OutputToken, false)
const html = await output.render(scope) const html = await output.render(scope)
return expect(html).to.equal('[object Object]') return expect(html).to.equal('[object Object]')
}) })
it('should respect to .toString()', async () => { it('should respect to .toString()', async () => {
const scope = new Scope({ obj: { toString: () => 'FOO' } }) const scope = new Context({ obj: { toString: () => 'FOO' } })
const output = new Output({ value: 'obj' } as OutputToken, false) const output = new Output({ value: 'obj' } as OutputToken, false)
const str = await output.render(scope) const str = await output.render(scope)
return expect(str).to.equal('FOO') return expect(str).to.equal('FOO')
}) })
it('should respect to .toString()', async () => { it('should respect to .toString()', async () => {
const scope = new Scope({ obj: { toString: () => 'FOO' } }) const scope = new Context({ obj: { toString: () => 'FOO' } })
const output = new Output({ value: 'obj' } as OutputToken, false) const output = new Output({ value: 'obj' } as OutputToken, false)
const str = await output.render(scope) const str = await output.render(scope)
return expect(str).to.equal('FOO') return expect(str).to.equal('FOO')
+12 -12
View File
@@ -1,6 +1,6 @@
import * as chai from 'chai' import * as chai from 'chai'
import Tag from '../../../src/template/tag/tag' import Tag from '../../../src/template/tag/tag'
import Scope from '../../../src/scope/scope' import Context from '../../../src/context/context'
import * as sinon from 'sinon' import * as sinon from 'sinon'
import * as sinonChai from 'sinon-chai' import * as sinonChai from 'sinon-chai'
import Liquid from '../../../src/liquid' import Liquid from '../../../src/liquid'
@@ -11,9 +11,9 @@ const expect = chai.expect
const liquid = new Liquid() const liquid = new Liquid()
describe('tag', function () { describe('tag', function () {
let scope: Scope let ctx: Context
before(function () { before(function () {
scope = new Scope({ ctx = new Context({
foo: 'bar', foo: 'bar',
arr: [2, 1], arr: [2, 1],
bar: { bar: {
@@ -51,7 +51,7 @@ describe('tag', function () {
value: 'foo', value: 'foo',
name: 'foo' name: 'foo'
} as TagToken } as TagToken
await new Tag(token, [], liquid).render(scope) await new Tag(token, [], liquid).render(ctx)
expect(spy).to.have.been.called expect(spy).to.have.been.called
}) })
@@ -70,30 +70,30 @@ describe('tag', function () {
} as TagToken } as TagToken
}) })
it('should call tag.render with scope', async function () { it('should call tag.render with scope', async function () {
await new Tag(token, [], liquid).render(scope) await new Tag(token, [], liquid).render(ctx)
expect(spy).to.have.been.calledWithMatch(scope) expect(spy).to.have.been.calledWithMatch(ctx)
}) })
it('should resolve identifier hash', async function () { it('should resolve identifier hash', async function () {
await new Tag(token, [], liquid).render(scope) await new Tag(token, [], liquid).render(ctx)
expect(spy).to.have.been.calledWithMatch({}, { expect(spy).to.have.been.calledWithMatch({}, {
aa: 'bar' aa: 'bar'
}) })
}) })
it('should accept space between key/value', async function () { it('should accept space between key/value', async function () {
await new Tag(token, [], liquid).render(scope) await new Tag(token, [], liquid).render(ctx)
expect(spy).to.have.been.calledWithMatch({}, { expect(spy).to.have.been.calledWithMatch({}, {
bb: 2 bb: 2
}) })
}) })
it('should resolve number value hash', async function () { it('should resolve number value hash', async function () {
await new Tag(token, [], liquid).render(scope) await new Tag(token, [], liquid).render(ctx)
expect(spy).to.have.been.calledWithMatch(scope, { expect(spy).to.have.been.calledWithMatch(ctx, {
cc: 2.3 cc: 2.3
}) })
}) })
it('should resolve property access hash', async function () { it('should resolve property access hash', async function () {
await new Tag(token, [], liquid).render(scope) await new Tag(token, [], liquid).render(ctx)
expect(spy).to.have.been.calledWithMatch(scope, { expect(spy).to.have.been.calledWithMatch(ctx, {
dd: 'uoo' dd: 'uoo'
}) })
}) })
+2 -2
View File
@@ -1,7 +1,7 @@
import * as chai from 'chai' import * as chai from 'chai'
import * as sinonChai from 'sinon-chai' import * as sinonChai from 'sinon-chai'
import * as sinon from 'sinon' import * as sinon from 'sinon'
import Scope from '../../../src/scope/scope' import Context from '../../../src/context/context'
import { Filter } from '../../../src/template/filter/filter' import { Filter } from '../../../src/template/filter/filter'
import Value from '../../../src/template/value' import Value from '../../../src/template/value'
@@ -105,7 +105,7 @@ describe('Value', function () {
Filter.register('date', date) Filter.register('date', date)
Filter.register('time', time) Filter.register('time', time)
const tpl = new Value('foo.bar | date: "b" | time:2', false) const tpl = new Value('foo.bar | date: "b" | time:2', false)
const scope = new Scope({ const scope = new Context({
foo: { bar: 'bar' } foo: { bar: 'bar' }
}) })
await tpl.value(scope) await tpl.value(scope)