Skip to content

Instantly share code, notes, and snippets.

@jpsullivan
Created April 11, 2013 18:01
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jpsullivan/5365684 to your computer and use it in GitHub Desktop.
Save jpsullivan/5365684 to your computer and use it in GitHub Desktop.
Precompile Handlebars templates using BundleTransformer
using System.Collections.Generic;
using System.Web.Optimization;
using BundleTransformer.Core.Orderers;
using BundleTransformer.Core.Transformers;
using BundleTransformer.Core.Translators;
using Ember;
namespace MyApp.App_Start
{
public class BundleConfig
{
// For more information on Bundling, visit http://go.microsoft.com/fwlink/?LinkId=254725
public static void RegisterBundles(BundleCollection bundles)
{
var cssTransformer = new CssTransformer();
var jsTransformer = new JsTransformer();
var nullOrderer = new NullOrderer();
...
// App JS
Bundle appJs = new ScriptBundle("~/bundles/js_app").IncludeDirectory("~/Content/js/app", "*.js", true)
appJs.Transforms.Add(jsTransformer);
appJs.Orderer = new NullOrderer();
bundles.Add(appJs);
// JST's
Bundle jsTemplates = new ScriptBundle("~/bundles/templates").IncludeDirectory("~/Content/js/app/views/jst", "*.handlebars", true);
bundles.Add(jsTemplates);
...
}
}
}
using System;
using System.Collections.Generic;
using System.Web;
using System.Web.Caching;
using System.Web.Script.Serialization;
using BundleTransformer.Core;
using BundleTransformer.Core.Assets;
using BundleTransformer.Core.Configuration;
using BundleTransformer.Core.FileSystem;
using BundleTransformer.Core.HttpHandlers;
using BundleTransformer.Core.Translators;
using BundleTransformer.Core.Web;
using MsieJavaScriptEngine;
using MsieJavaScriptEngine.ActiveScript;
using dotless.Core.Input;
namespace MyApp.Infrastructure.Optimization {
public class HandlebarsAssetHandler : AssetHandlerBase {
public override string ContentType {
get { return BundleTransformer.Core.Constants.ContentType.Js; }
}
/// <summary>
/// Constructs instance of Handlebars asset handler
/// </summary>
public HandlebarsAssetHandler()
: this(HttpContext.Current.Cache,
BundleTransformerContext.Current.GetVirtualFileSystemWrapper(),
BundleTransformerContext.Current.GetCoreConfiguration().AssetHandler,
BundleTransformerContext.Current.GetApplicationInfo())
{ }
/// <summary>
/// Constructs instance of Handlebars asset handler
/// </summary>
/// <param name="cache">Server cache</param>
/// <param name="virtualFileSystemWrapper">Virtual file system wrapper</param>
/// <param name="assetHandlerConfig">Configuration settings of HTTP-handler that responsible
/// for text output of processed asset</param>
/// <param name="applicationInfo">Information about web application</param>
private HandlebarsAssetHandler(Cache cache, IVirtualFileSystemWrapper virtualFileSystemWrapper,
AssetHandlerSettings assetHandlerConfig, IHttpApplicationInfo applicationInfo)
: base(cache, virtualFileSystemWrapper, assetHandlerConfig, applicationInfo)
{ }
/// <summary>
/// Translates code of asset written on Handlebars to JS-code
/// </summary>
/// <param name="asset">Asset with code written on CoffeeScript</param>
/// <returns>Asset with translated code</returns>
protected override IAsset ProcessAsset(IAsset asset)
{
ITranslator handlebarsTranslator = BundleTransformerContext.Current.GetJsTranslatorInstance("HandlebarsTranslator");
handlebarsTranslator.IsDebugMode = _applicationInfo.IsDebugMode;
return handlebarsTranslator.Translate(asset);
}
}
public class HandlebarsTranslator : TranslatorWithNativeMinificationBase
{
private readonly MsieJsEngine _jsEngine = new MsieJsEngine();
/// <summary>
/// Gets or sets a flag that web application is in debug mode
/// </summary>
public bool IsDebugMode
{
get;
set;
}
/// <summary>
/// Translates code of asset written on CoffeeScript to JS-code
/// </summary>
/// <param name="asset">Asset with code written on CoffeeScript</param>
/// <returns>Asset with translated code</returns>
public override IAsset Translate(IAsset asset)
{
if (asset == null) {
throw new ArgumentException("Value cannot be empty.", "asset");
}
using (var handlebarsCompiler = new HandlebarsCompiler()) {
InnerTranslate(asset, handlebarsCompiler);
}
return asset;
}
/// <summary>
/// Translates code of assets written on CoffeeScript to JS-code
/// </summary>
/// <param name="assets">Set of assets with code written on CoffeeScript</param>
/// <returns>Set of assets with translated code</returns>
public override IList<IAsset> Translate(IList<IAsset> assets)
{
if (assets == null) {
throw new ArgumentException("Value cannot be empty.", "assets");
}
if (assets.Count == 0) {
return assets;
}
using (var handlebarsCompiler = new HandlebarsCompiler()) {
foreach (var asset in assets) {
InnerTranslate(asset, handlebarsCompiler);
}
}
return assets;
}
private static void InnerTranslate(IAsset asset, HandlebarsCompiler handlebarsCompiler)
{
string newContent;
try
{
newContent = handlebarsCompiler.Compile(asset.Content, asset.VirtualPath);
}
catch (Exception e)
{
// throw new AssetTranslationException(
// string.Format(CoreStrings.Translators_TranslationFailed,
// INPUT_CODE_TYPE, OUTPUT_CODE_TYPE, assetVirtualPath, e.Message));
throw e;
}
asset.Content = newContent;
}
}
internal sealed class HandlebarsCompiler : IDisposable
{
/// <summary>
/// Name of resource, which contains a CoffeeScript-library
/// </summary>
const string HandlebarsLibraryVirtualPath = "~/Content/js/lib/handlebars.js";
/// <summary>
/// Template of function call, which is responsible for compilation
/// </summary>
const string CompilationFunctionCallTemplate = "Handlebars.precompile({0}, {{ knownHelpers : ['t', 'eachkeys', 'ifCond'], knownHelpersOnly: false }});";
/// <summary>
/// Window-level namespace name for storing each Handlebars template
/// </summary>
private const string HandlebarsTemplateNamespace = "if (typeof {0}==='undefined'){{var {0}={{}};}} {0}[\"{1}\"] = {2}\n";
/// <summary>
/// MSIE JS engine
/// </summary>
private MsieJsEngine _jsEngine;
/// <summary>
/// Synchronizer of compilation
/// </summary>
private readonly object _compilationSynchronizer = new object();
/// <summary>
/// JS-serializer
/// </summary>
private readonly JavaScriptSerializer _jsSerializer;
/// <summary>
/// Flag that compiler is initialized
/// </summary>
private bool _initialized;
/// <summary>
/// Flag that object is destroyed
/// </summary>
private bool _disposed;
/// <summary>
/// Constructs instance of CoffeeScript-compiler
/// </summary>
public HandlebarsCompiler()
{
_jsSerializer = new JavaScriptSerializer();
}
/// <summary>
/// Destructs instance of CoffeeScript-compiler
/// </summary>
~HandlebarsCompiler()
{
Dispose(false /* disposing */);
}
/// <summary>
/// Initializes compiler
/// </summary>
private void Initialize()
{
if (!_initialized)
{
var server = new AspServerPathResolver();
_jsEngine = new MsieJsEngine(true);
_jsEngine.ExecuteFile(server.GetFullPath(HandlebarsLibraryVirtualPath));
_initialized = true;
}
}
/// <summary>
/// "Compiles" CoffeeScript-code to JS-code
/// </summary>
/// <param name="content">Text content written on CoffeeScript</param>
/// <param name="assetPath"></param>
/// <param name="handlebarsNamespace"></param>
/// <returns>Translated CoffeeScript-code</returns>
public string Compile(string content, string assetPath, string handlebarsNamespace = "JST")
{
string newContent = null;
var options = new
{
JSTNamespace = handlebarsNamespace,
PathReplace = "/Content/js/app/views/jst/"
};
lock (_compilationSynchronizer)
{
Initialize();
try
{
var compileString = string.Format(CompilationFunctionCallTemplate, _jsSerializer.Serialize(content));
var compiledTemplate = _jsEngine.Evaluate<string>(compileString);
newContent = string.Format(HandlebarsTemplateNamespace, options.JSTNamespace, FormatAssetSourcePath(options.PathReplace, assetPath, true), compiledTemplate);
}
catch (ActiveScriptException e)
{
//throw new HandlebarsCompilingException(ActiveScriptErrorFormatter.Format(e));
}
}
return newContent;
}
/// <summary>
/// Cleans up the asset path so that referencing JST's is relative to the "views" path where
/// all the client's JST's are stored
/// </summary>
/// <param name="pathReplace"></param>
/// <param name="assetPath"></param>
/// <param name="removeExtension"></param>
/// <returns></returns>
private string FormatAssetSourcePath(string pathReplace, string assetPath, bool removeExtension) {
string output = assetPath.Replace(pathReplace, string.Empty);
if (removeExtension)
{
var index = output.IndexOf('.');
if (index > 0) output = output.Substring(0, index);
}
return output.TrimStart('/');
}
/// <summary>
/// Destroys object
/// </summary>
public void Dispose()
{
Dispose(true /* disposing */);
GC.SuppressFinalize(this);
}
/// <summary>
/// Destroys object
/// </summary>
/// <param name="disposing">Flag, allowing destruction of
/// managed objects contained in fields of class</param>
private void Dispose(bool disposing)
{
if (!_disposed)
{
_disposed = true;
if (_jsEngine != null)
{
_jsEngine.Dispose();
}
}
}
}
}
<system.web>
...
<httpHandlers>
<add path="*.less" verb="GET" type="BundleTransformer.Less.HttpHandlers.LessAssetHandler, BundleTransformer.Less" />
<add path="*.handlebars" verb="GET" type="MyApp.Infrastructure.Optimization.HandlebarsAssetHandler, MyApp" />
</httpHandlers>
...
</system.web>
<system.webServer>
<validation validateIntegratedModeConfiguration="false" />
<handlers>
...
<add name="LessAssetHandler" path="*.less" verb="GET" type="BundleTransformer.Less.HttpHandlers.LessAssetHandler, BundleTransformer.Less" resourceType="File" preCondition="" />
<add name="HandlebarsAssetHandler" path="*.handlebars" verb="GET" type="MyApp.Infrastructure.Optimization.HandlebarsAssetHandler, MyApp" resourceType="File" preCondition="" />
...
</handlers>
<staticContent>
<remove fileExtension=".svg" />
<remove fileExtension=".eot" />
<remove fileExtension=".woff" />
<remove fileExtension=".hbs" />
<mimeMap fileExtension=".svg" mimeType="image/svg+xml" />
<mimeMap fileExtension=".eot" mimeType="application/vnd.ms-fontobject" />
<mimeMap fileExtension=".woff" mimeType="application/x-woff" />
<mimeMap fileExtension=".hbs" mimeType="text/javascript" />
</staticContent>
</system.webServer>
...
<bundleTransformer xmlns="http://tempuri.org/BundleTransformer.Configuration.xsd">
<core>
...
<js defaultMinifier="UglifyJsMinifier" usePreMinifiedFiles="false">
...
<translators>
<add name="NullTranslator" type="BundleTransformer.Core.Translators.NullTranslator, BundleTransformer.Core" enabled="false" />
<add name="HandlebarsTranslator" type="MyApp.Infrastructure.Optimization.HandlebarsTranslator, MyApp" enabled="true" />
</translators>
</js>
<assetHandler clientCacheDurationInDays="365" enableCompression="true" serverCacheDurationInMinutes="15" useServerCacheSlidingExpiration="false" disableClientCacheInDebugMode="true" disableCompressionInDebugMode="true" />
</core>
...
</bundleTransformer>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment