UNPKG

liquid-node

Version:

Node.js port of Tobias Lütke's Liquid template engine.

260 lines (211 loc) 7.84 kB
Liquid = require "../liquid" module.exports = class Context constructor: (engine, environments = {}, outerScope = {}, registers = {}, rethrowErrors = false) -> @environments = Liquid.Helpers.flatten [environments] @scopes = [outerScope] @registers = registers @errors = [] @rethrowErrors = rethrowErrors @strainer = new engine?.Strainer(@) ? {} @squashInstanceAssignsWithEnvironments() # Adds filters to this context. # # Note that this does not register the filters with the main # Template object. see <tt>Template.register_filter</tt> # for that registerFilters: (filters...) -> for filter in filters for own k, v of filter @strainer[k] = v if v instanceof Function return handleError: (e) -> @errors.push e throw e if @rethrowErrors if e instanceof Liquid.SyntaxError "Liquid syntax error: #{e.message}" else "Liquid error: #{e.message}" invoke: (methodName, args...) -> method = @strainer[methodName] if method instanceof Function method.apply @strainer, args else available = Object.keys @strainer throw new Liquid.FilterNotFound "Unknown filter `#{methodName}`, available: [#{available.join(', ')}]" push: (newScope = {}) -> @scopes.unshift newScope throw new Error("Nesting too deep") if @scopes.length > 100 merge: (newScope = {}) -> for own k, v of newScope @scopes[0][k] = v pop: -> throw new Error("ContextError") if @scopes.length <= 1 @scopes.shift() lastScope: -> @scopes[@scopes.length - 1] # Pushes a new local scope on the stack, pops it at the end of the block # # Example: # context.stack do # context['var'] = 'hi' # end # # context['var] #=> nil stack: (newScope = {}, f) -> popLater = false try if arguments.length < 2 f = newScope newScope = {} @push(newScope) result = f() if result?.nodeify? popLater = true result.nodeify => @pop() result finally @pop() unless popLater clearInstanceAssigns: -> @scopes[0] = {} # Only allow String, Numeric, Hash, Array, Proc, Boolean # or <tt>Liquid::Drop</tt> set: (key, value) -> @scopes[0][key] = value get: (key) -> @resolve(key) hasKey: (key) -> Promise.resolve(@resolve(key)).then (v) -> v? # PRIVATE API @Literals = 'null': null 'nil': null '': null 'true': true 'false': false # Look up variable, either resolve directly after considering the name. # We can directly handle Strings, digits, floats and booleans (true,false). # If no match is made we lookup the variable in the current scope and # later move up to the parent blocks to see if we can resolve # the variable somewhere up the tree. # Some special keywords return symbols. Those symbols are to be called on the rhs object in expressions # # Example: # products == empty #=> products.empty? resolve: (key) -> if Liquid.Context.Literals.hasOwnProperty key Liquid.Context.Literals[key] else if match = /^'(.*)'$/.exec(key) # Single quoted strings match[1] else if match = /^"(.*)"$/.exec(key) # Double quoted strings match[1] else if match = /^(\d+)$/.exec(key) # Integer and floats Number(match[1]) else if match = /^\((\S+)\.\.(\S+)\)$/.exec(key) # Ranges lo = @resolve(match[1]) hi = @resolve(match[2]) Promise.all([lo, hi]).then ([lo, hi]) -> lo = Number lo hi = Number hi return [] if isNaN(lo) or isNaN(hi) new Liquid.Range(lo, hi + 1) else if match = /^(\d[\d\.]+)$/.exec(key) # Floats Number(match[1]) else @variable(key) findVariable: (key) -> variableScope = undefined variable = undefined @scopes.some (scope) -> if scope.hasOwnProperty key variableScope = scope true unless variableScope? @environments.some (env) => variable = @lookupAndEvaluate env, key variableScope = env if variable? unless variableScope? if @environments.length > 0 variableScope = @environments[@environments.length - 1] else if @scopes.length > 0 variableScope = @scopes[@scopes.length - 1] else throw new Error "No scopes to find variable in." variable ?= @lookupAndEvaluate(variableScope, key) Promise.resolve(variable).then (v) => @liquify v variable: (markup) -> Promise.resolve().then => parts = Liquid.Helpers.scan(markup, Liquid.VariableParser) squareBracketed = /^\[(.*)\]$/ firstPart = parts.shift() if match = squareBracketed.exec(firstPart) firstPart = match[1] object = @findVariable(firstPart) return object if parts.length is 0 mapper = (part, object) => return Promise.resolve(object) unless object? Promise.resolve(object).then(@liquify.bind(@)).then (object) => return object unless object? bracketMatch = squareBracketed.exec part part = @resolve(bracketMatch[1]) if bracketMatch Promise.resolve(part).then (part) => isArrayAccess = (Array.isArray(object) and isFinite(part)) isObjectAccess = (object instanceof Object and (object.hasKey?(part) or part of object)) isSpecialAccess = ( !bracketMatch and object and (Array.isArray(object) or Object::toString.call(object) is "[object String]") and ["size", "first", "last"].indexOf(part) >= 0 ) if isArrayAccess or isObjectAccess # If object is a hash- or array-like object we look for the # presence of the key and if its available we return it Promise.resolve(@lookupAndEvaluate(object, part)).then(@liquify.bind(@)) else if isSpecialAccess # Some special cases. If the part wasn't in square brackets # and no key with the same name was found we interpret # following calls as commands and call them on the # current object switch part when "size" @liquify(object.length) when "first" @liquify(object[0]) when "last" @liquify(object[object.length - 1]) else ### @covignore ### throw new Error "Unknown special accessor: #{part}" # The iterator walks through the parsed path step # by step and waits for promises to be fulfilled. iterator = (object, index) -> if index < parts.length mapper(parts[index], object).then (object) -> iterator(object, index + 1) else Promise.resolve(object) iterator(object, 0).catch (err) -> throw new Error "Couldn't walk variable: #{markup}: #{err}" lookupAndEvaluate: (obj, key) -> if obj instanceof Liquid.Drop obj.get(key) else obj?[key] squashInstanceAssignsWithEnvironments: -> lastScope = @lastScope() Object.keys(lastScope).forEach (key) => @environments.some (env) => if env.hasOwnProperty key lastScope[key] = @lookupAndEvaluate env, key true liquify: (object) -> Promise.resolve(object).then (object) => unless object? return object else if typeof object.toLiquid is "function" object = object.toLiquid() else if typeof object is "object" true # throw new Error "Complex object #{JSON.stringify(object)} would leak into template." else if typeof object is "function" object = "" else Object::toString.call object object.context = @ if object instanceof Liquid.Drop object