Created
November 4, 2012 22:12
-
-
Save vjpr/4013997 to your computer and use it in GitHub Desktop.
Asynchronous template compilation.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
logger = require('onelog').get('AsyncTemplates') | |
_ = require 'underscore' | |
sinon = require 'sinon' | |
async = require 'async' | |
hamlc = require 'haml-coffee' | |
# You MUST specify the ALL names of the methods which return asynchronously. | |
# This is required because sync helpers can be used in conditionals. | |
# This is useful for retrofitting existing code, or keeping code clean | |
# and portable. | |
asyncMethods = ['js', 'css', 'asset', 'assetPath'] | |
hamlcAssetHelperRegex = /(@js|@css|@asset)(.?)'(.*)'/gi | |
# Extract required assets from template by regex. | |
exports.requiredAssets = (str) => | |
matches = [] | |
loop | |
match = hamlcAssetHelperRegex.exec str | |
break unless match? | |
matches.push match[3] if match.length >= 4 | |
return matches | |
# TODO: Provide option to manually specify which helpers are asynchronous. | |
exports.renderAsync = (opts, cb) => | |
data = opts.data | |
realLocals = opts.locals | |
# Method that compiles a template to a function that compiles locals to html. | |
# `string` -> `(locals) -> html` | |
templater = opts.templater | |
# Method that compiles a template and locals to html. | |
# `(data, locals) -> html` | |
compiler = opts.compiler | |
start = new Date() | |
# Pre-render template. | |
locals = _.clone realLocals | |
# Only stub asynchronous calls. Synchronous helpers maybe used in conditional | |
# statements with nested async helpers. | |
# NOTE: Cannot use async helpers for conditionals for blocks containing more | |
# async helpers. | |
for name, fn of locals | |
if _.contains asyncMethods, name | |
sinon.stub locals, name | |
else | |
sinon.spy locals, name | |
# Compile template method. | |
tmpl = null | |
if templater? | |
tmpl = templater data | |
else | |
tmpl = (locals) -> compiler data, locals | |
# 1st run to spy on which methods from locals are required for rendering. | |
tmpl locals | |
evaluatedLocals = {} # [method name][ordering of call] | |
tasks = [] # Async tasks to be run. | |
for name, spy of locals | |
if spy.called | |
# Evaluate local for each time it was called. | |
for i in [0..spy.callCount - 1] | |
do (name, spy, i) -> | |
tasks.push | |
name: "#{name}##{i}" | |
args: spy.args[i] | |
run: (taskFinished) -> | |
done = (err, val) -> | |
return taskFinished err if err | |
evaluatedLocals[name] = {} unless evaluatedLocals[name]? | |
# Store the evaluated method from each call. | |
evaluatedLocals[name][i] = do (val) -> val | |
taskFinished() | |
logger.trace "Evaluating #{name}() with args:", spy.args[i] | |
# We need a way to check if a method is sync or async. | |
# No callback will be called if its sync. | |
# We could require all helpers to be async, however the majority of | |
# helpers are synchronous, so it should be opt-in from the template. | |
val = undefined | |
if _.isFunction _.last(spy.args[i]) | |
# Template helper is asynchronous. | |
val = realLocals[name] _.initial(spy.args[i])..., done | |
else if _.contains asyncMethods, name | |
# Template helper is also asynchronous, but last arg is not | |
# a function, the name of the method was preset as async. | |
val = realLocals[name] spy.args[i]..., done | |
else | |
val = realLocals[name] spy.args[i]... | |
done null, val | |
# Wait until all locals have been evaluated. | |
async.forEach tasks, (task, done) -> | |
logger.trace 'Running task', task.name | |
task.run done | |
, (err) -> | |
if err | |
logger.error err | |
return cb err | |
# Create a new stub that responds to our calls with the evaluated local | |
# for the n-th call. | |
stubLocals = {} | |
for name in _.keys evaluatedLocals | |
# Create a closure to share `n` amongst all invocations of each | |
# method: `name`. This allows us to pass different args to the same | |
# method call. | |
do (name) -> | |
n = 0 | |
stubLocals[name] = -> | |
#logger.trace "Rendered #{name}##{n}", evaluatedLocals[name][n] | |
str = evaluatedLocals[name][n] | |
++n | |
return str | |
# 2nd run to get html. | |
html = tmpl stubLocals | |
duration = (new Date() - start) / 1000 | |
logger.info "Rendered template in #{duration}s" | |
cb null, html |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment