UNPKG

liquid

Version:

Node.js port of the Liquid template engine

301 lines (261 loc) 7.64 kB
const Liquid = require('../liquid') const squareBracketed = /^\[(.*)\]$/ class Context { constructor ( engine, environments = {}, outerScope = {}, registers = {}, rethrowErrors = false ) { this.environments = Liquid.Helpers.flatten([environments]) this.scopes = [outerScope] this.registers = registers this.errors = [] this.rethrowErrors = rethrowErrors this.strainer = engine ? new engine.Strainer(this) : {} this.squashInstanceAssignsWithEnvironments() } registerFilters (...filters) { for (const filter of filters) { for (const key in filter) { if (!Object.prototype.hasOwnProperty.call(filter, key)) continue const value = filter[key] if (value instanceof Function) { this.strainer[key] = value } } } } handleError (e) { this.errors.push(e) if (this.rethrowErrors) { throw e } if (e instanceof Liquid.SyntaxError) { return 'Liquid syntax error: ' + e.message } else { return 'Liquid error: ' + e.message } } invoke (methodName, ...args) { const method = this.strainer[methodName] if (method instanceof Function) { return method.apply(this.strainer, args) } else { const available = Object.keys(this.strainer) throw new Liquid.FilterNotFound('Unknown filter `' + methodName + '`, available: [' + (available.join(', ')) + ']') } } push (newScope = {}) { this.scopes.unshift(newScope) if (this.scopes.length > 100) { throw new Error('Nesting too deep') } } merge (newScope = {}) { const results = [] for (const key in newScope) { if (!Object.prototype.hasOwnProperty.call(newScope, key)) continue const v = newScope[key] results.push(this.scopes[0][key] = v) } return results } pop () { if (this.scopes.length <= 1) { throw new Error('ContextError') } return this.scopes.shift() } lastScope () { return this.scopes[this.scopes.length - 1] } stack (newScope = {}, f) { let popLater = false try { if (arguments.length < 2) { f = newScope newScope = {} } this.push(newScope) const result = f() if (result && result.nodeify) { popLater = true result.nodeify(() => this.pop()) } return result } finally { if (!popLater) { this.pop() } } } clearInstanceAssigns () { this.scopes[0] = {} } set (key, value) { this.scopes[0][key] = value } async get (key) { return this.resolve(key) } async hasKey (key) { const value = await this.resolve(key) return value != null } async resolve (key) { let match if (Liquid.Context.Literals.hasOwnProperty(key)) { return Liquid.Context.Literals[key] } else if (match = /^'(.*)'$/.exec(key)) { // eslint-disable-line return match[1] } else if (match = /^"(.*)"$/.exec(key)) { // eslint-disable-line return match[1] } else if (match = /^(\d+)$/.exec(key)) { // eslint-disable-line return Number(match[1]) } else if (match = /^\((\S+)\.\.(\S+)\)$/.exec(key)) { // eslint-disable-line const loHi = [match[1], match[2]] const [loArg, hiArg] = await Promise.all(loHi.map(async arg => { const value = await this.resolve(arg) return Number(value) })) if (isNaN(loArg) || isNaN(hiArg)) { return [] } return new Liquid.Range(loArg, hiArg + 1) } else if (match = /^(\d[\d.]+)$/.exec(key)) { // eslint-disable-line return Number(match[1]) } else { return this.variable(key) } } async findVariable (key) { let variableScope = this.scopes.find((scope) => { return Object.prototype.hasOwnProperty.call(scope, key) }) let variable if (variableScope == null) { this.environments.some(env => { variable = this.lookupAndEvaluate(env, key) if (variable != null) { variableScope = env return variableScope } }) } if (variableScope == null) { if (this.environments.length > 0) { variableScope = this.environments[this.environments.length - 1] } else if (this.scopes.length > 0) { variableScope = this.scopes[this.scopes.length - 1] } else { throw new Error('No scopes to find variable in.') } } if (variable == null) { variable = this.lookupAndEvaluate(variableScope, key) } return this.liquify(variable) } async mapper (part, object) { if (object == null) { return object } object = await this.liquify(object) if (object == null) { return object } const bracketMatch = squareBracketed.exec(part) if (bracketMatch) { part = await this.resolve(bracketMatch[1]) } const isArrayAccess = Array.isArray(object) && isFinite(part) const isObjectAccess = object instanceof Object && ((typeof object.hasKey === 'function' ? object.hasKey(part) : void 0) || part in object) const isSpecialAccess = !bracketMatch && object && (Array.isArray(object) || Object.prototype.toString.call(object) === '[object String]') && ['size', 'first', 'last'].indexOf(part) >= 0 if (isArrayAccess || isObjectAccess) { const evaluated = await this.lookupAndEvaluate(object, part) return this.liquify(evaluated) } else if (isSpecialAccess) { switch (part) { case 'size': return this.liquify(object.length) case 'first': return this.liquify(object[0]) case 'last': return this.liquify(object[object.length - 1]) default: /* @covignore */ throw new Error('Unknown special accessor: ' + part) } } } async variable (markup) { const parts = Liquid.Helpers.scan(markup, Liquid.VariableParser) let firstPart = parts.shift() const match = squareBracketed.exec(firstPart) if (match) { firstPart = match[1] } const object = await this.findVariable(firstPart) if (parts.length === 0) { return object } const iterator = async (object, index) => { if (index < parts.length) { const o = await this.mapper(parts[index], object) return iterator(o, index + 1) } else { return object } } try { return iterator(object, 0) } catch (err) { throw new Error("Couldn't walk variable: " + markup + ': ' + err) } } lookupAndEvaluate (obj, key) { if (obj instanceof Liquid.Drop) { return obj.get(key) } else { return obj != null ? obj[key] : void 0 } } squashInstanceAssignsWithEnvironments () { const lastScope = this.lastScope() return Object.keys(lastScope).forEach(key => { return this.environments.some(env => { if (env.hasOwnProperty(key)) { lastScope[key] = this.lookupAndEvaluate(env, key) return true } }) }) } async liquify (object) { if (object == null) { return object } else if (typeof object.toLiquid === 'function') { object = object.toLiquid() } else if (typeof object === 'object') { true // eslint-disable-line } else if (typeof object === 'function') { object = '' } else { Object.prototype.toString.call(object) } if (object instanceof Liquid.Drop) { object.context = this } return object } } Context.Literals = { 'null': null, 'nil': null, '': null, 'true': true, 'false': false } module.exports = Context