Skip to content

Instantly share code, notes, and snippets.

@tealtail
Created July 19, 2013 15:12
Show Gist options
  • Save tealtail/6039828 to your computer and use it in GitHub Desktop.
Save tealtail/6039828 to your computer and use it in GitHub Desktop.
Handlebars Bundle Transformer class for compiling Handlebars with ASP.NET MVC4 Bundles. Works well, so I thought I'd save it for reuse.
using System;
using System.Collections.Generic;
using System.Web;
using System.Web.Caching;
using System.Web.Script.Serialization;
using BundleTransformer.Core;
using System.Linq;
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;
using CoreStrings = BundleTransformer.Core.Resources.Strings;
namespace CollexAdmin.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.GetFileSystemWrapper(),
BundleTransformerContext.Current.GetCoreConfiguration().AssetHandler,
BundleTransformerContext.Current.GetApplicationInfo())
{ }
/// <summary>
/// Constructs instance of Handlebars asset handler
/// </summary>
/// <param name="cache">Server cache</param>
/// <param name="fileSystemWrapper">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, IFileSystemWrapper fileSystemWrapper,
AssetHandlerSettings assetHandlerConfig, IHttpApplicationInfo applicationInfo)
: base(cache, fileSystemWrapper, assetHandlerConfig, applicationInfo)
{ }
/// <summary>
/// Translates code of asset written on Handlebars to JS-code
/// </summary>
/// <param name="asset">Asset with code written on Handlebars</param>
/// <returns>Asset with translated code</returns>
protected override IAsset ProcessAsset(IAsset asset)
{
ITranslator handlebarsTranslator = BundleTransformerContext.Current.GetJsTranslatorInstance("HandlebarsTranslator");
handlebarsTranslator.IsDebugMode = _applicationInfo.IsDebugMode;
IAsset processedAsset = handlebarsTranslator.Translate(asset);
return processedAsset;
}
}
public sealed class HandlebarsTranslator : ITranslator
{
private readonly MsieJsEngine _jsEngine = new MsieJsEngine();
/// <summary>
/// Name of input code type
/// </summary>
const string INPUT_CODE_TYPE = "Handlebars";
/// <summary>
/// Name of output code type
/// </summary>
const string OUTPUT_CODE_TYPE = "JS";
/// <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 Handlebars to JS-code
/// </summary>
/// <param name="asset">Asset with code written on Handlebars</param>
/// <returns>Asset with translated code</returns>
public IAsset Translate(IAsset asset)
{
if (asset == null)
{
throw new ArgumentException(CoreStrings.Common_ValueIsEmpty, "asset");
}
using (var handlebarsCompiler = new HandlebarsCompiler())
{
InnerTranslate(asset, handlebarsCompiler);
}
return asset;
}
/// <summary>
/// Translates code of assets written on Handlebars to JS-code
/// </summary>
/// <param name="assets">Set of assets with code written on Handlebars</param>
/// <returns>Set of assets with translated code</returns>
public IList<IAsset> Translate(IList<IAsset> assets)
{
if (assets == null)
{
throw new ArgumentException(CoreStrings.Common_ValueIsEmpty, "assets");
}
if (assets.Count == 0)
{
return assets;
}
var assetsToProcessing = assets.Where(a => a.AssetType == AssetType.Handlebars).ToList();
if (assetsToProcessing.Count == 0)
{
return assets;
}
using (var handlebarsCompiler = new HandlebarsCompiler())
{
foreach (var asset in assetsToProcessing)
{
InnerTranslate(asset, handlebarsCompiler);
}
}
return assets;
}
private void InnerTranslate(IAsset asset, HandlebarsCompiler handlebarsCompiler)
{
string newContent;
try
{
newContent = handlebarsCompiler.Compile(asset.Content, asset.Url);
}
catch (Exception e)
{
throw new AssetTranslationException(
string.Format(CoreStrings.Translators_TranslationFailed,
INPUT_CODE_TYPE, OUTPUT_CODE_TYPE, asset.Url, e.Message));
}
asset.Content = newContent;
}
}
internal sealed class HandlebarsCompiler : IDisposable
{
/// <summary>
/// Name of resource, which contains a Handlebars-library
/// </summary>
const string HandlebarsLibraryVirtualPath = "~/scripts/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}\"] = Handlebars.template({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 Handlebars-compiler
/// </summary>
public HandlebarsCompiler()
{
_jsSerializer = new JavaScriptSerializer();
}
/// <summary>
/// Destructs instance of Handlebars-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" Handlebars-code to JS-code
/// </summary>
/// <param name="content">Text content written on Handlebars</param>
/// <param name="assetPath"></param>
/// <param name="handlebarsNamespace"></param>
/// <returns>Translated Handlebars-code</returns>
public string Compile(string content, string assetPath, string handlebarsNamespace = "JST")
{
string newContent = null;
var options = new
{
JSTNamespace = handlebarsNamespace,
PathReplace = "/js/templates/"
};
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();
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment