From 0de8dac702f7b06161a0c5464a80bd327d708258 Mon Sep 17 00:00:00 2001
From: kpdecker <kpdecker@gmail.com>
Date: Tue, 4 Aug 2015 12:34:28 -0500
Subject: [PATCH] Add support for string and stdin precompilation

Fixes #1071
---
 bin/handlebars      | 11 +++++++
 lib/precompiler.js  | 76 ++++++++++++++++++++++++++++++++++++++++++---
 package.json        |  1 +
 spec/precompiler.js | 46 +++++++++++++++++++++++++++
 4 files changed, 130 insertions(+), 4 deletions(-)

diff --git a/bin/handlebars b/bin/handlebars
index bfe6680d3..7645adf36 100755
--- a/bin/handlebars
+++ b/bin/handlebars
@@ -54,6 +54,16 @@ var optimist = require('optimist')
         'description': 'Output template function only.',
         'alias': 'simple'
       },
+      'N': {
+        'type': 'string',
+        'description': 'Name of passed string templates. Optional if running in a simple mode. Required when operating on multiple templates.',
+        'alias': 'name'
+      },
+      'i': {
+        'type': 'string',
+        'description': 'Generates a template from the passed CLI argument.\n"-" is treated as a special value and causes stdin to be read for the template value.',
+        'alias': 'string'
+      },
       'r': {
         'type': 'string',
         'description': 'Template root. Base value that will be stripped from template names.',
@@ -92,6 +102,7 @@ var optimist = require('optimist')
       }
     })
 
+    .wrap(120)
     .check(function(argv) {
       if (argv.version) {
         return;
diff --git a/lib/precompiler.js b/lib/precompiler.js
index 12a5a1f96..9f23ef661 100644
--- a/lib/precompiler.js
+++ b/lib/precompiler.js
@@ -7,12 +7,64 @@ import {SourceMapConsumer, SourceNode} from 'source-map';
 import uglify from 'uglify-js';
 
 module.exports.loadTemplates = function(opts, callback) {
+  loadStrings(opts, function(err, strings) {
+    if (err) {
+      callback(err);
+    } else {
+      loadFiles(opts, function(err, files) {
+        if (err) {
+          callback(err);
+        } else {
+          opts.templates = strings.concat(files);
+          callback(undefined, opts);
+        }
+      });
+    }
+  });
+};
+
+function loadStrings(opts, callback) {
+  let strings = arrayCast(opts.string),
+      names = arrayCast(opts.name);
+
+  if (names.length !== strings.length
+      && strings.length > 1) {
+    return callback(new Handlebars.Exception('Number of names did not match the number of string inputs'));
+  }
+
+  Async.map(strings, function(string, callback) {
+      if (string !== '-') {
+        callback(undefined, string);
+      } else {
+        // Load from stdin
+        let buffer = '';
+        process.stdin.setEncoding('utf8');
+
+        process.stdin.on('data', function(chunk) {
+          buffer += chunk;
+        });
+        process.stdin.on('end', function() {
+          callback(undefined, buffer);
+        });
+      }
+    },
+    function(err, strings) {
+      strings = strings.map((string, index) => ({
+        name: names[index],
+        path: names[index],
+        source: string
+      }));
+      callback(err, strings);
+    });
+}
+
+function loadFiles(opts, callback) {
   // Build file extension pattern
   let extension = (opts.extension || 'handlebars').replace(/[\\^$*+?.():=!|{}\-\[\]]/g, function(arg) { return '\\' + arg; });
   extension = new RegExp('\\.' + extension + '$');
 
   let ret = [],
-      queue = opts.files.map((template) => ({template, root: opts.root}));
+      queue = (opts.files || []).map((template) => ({template, root: opts.root}));
   Async.whilst(() => queue.length, function(callback) {
     let {template: path, root} = queue.shift();
 
@@ -74,9 +126,7 @@ module.exports.loadTemplates = function(opts, callback) {
     if (err) {
       callback(err);
     } else {
-      opts.templates = ret;
-
-      callback(undefined, opts);
+      callback(undefined, ret);
     }
   });
 }
@@ -100,6 +150,12 @@ module.exports.cli = function(opts) {
     throw new Handlebars.Exception('Unable to output multiple templates in simple mode');
   }
 
+  // Force simple mode if we have only one template and it's unnamed.
+  if (!opts.amd && !opts.commonjs && opts.templates.length === 1
+      && !opts.templates[0].name) {
+    opts.simple = true;
+  }
+
   // Convert the known list into a hash
   let known = {};
   if (opts.known && !Array.isArray(opts.known)) {
@@ -156,6 +212,10 @@ module.exports.cli = function(opts) {
     if (opts.simple) {
       output.add([precompiled, '\n']);
     } else {
+      if (!template.name) {
+        throw new Handlebars.Exception('Name missing for template');
+      }
+
       if (opts.amd && !multiple) {
         output.add('return ');
       }
@@ -206,3 +266,11 @@ module.exports.cli = function(opts) {
     console.log(output);
   }
 };
+
+function arrayCast(value) {
+  value = value != null ? value : [];
+  if (!Array.isArray(value)) {
+    value = [value];
+  }
+  return value;
+}
diff --git a/package.json b/package.json
index b1cafeeae..8548cd867 100644
--- a/package.json
+++ b/package.json
@@ -53,6 +53,7 @@
     "jison": "~0.3.0",
     "keen.io": "0.0.3",
     "mocha": "~1.20.0",
+    "mock-stdin": "^0.3.0",
     "mustache": "0.x",
     "semver": "^4.0.0",
     "underscore": "^1.5.1"
diff --git a/spec/precompiler.js b/spec/precompiler.js
index 25c57a846..e1ad5ade9 100644
--- a/spec/precompiler.js
+++ b/spec/precompiler.js
@@ -71,6 +71,11 @@ describe('precompiler', function() {
       Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars', __dirname + '/artifacts/empty.handlebars'], simple: true});
     }, Handlebars.Exception, 'Unable to output multiple templates in simple mode');
   });
+  it('should throw when missing name', function() {
+    shouldThrow(function() {
+      Precompiler.cli({templates: [{source: ''}], amd: true});
+    }, Handlebars.Exception, 'Name missing for template');
+  });
   it('should throw when combining simple and directories', function() {
     shouldThrow(function() {
       Precompiler.cli({hasDirectory: true, templates: [1], simple: true});
@@ -82,6 +87,11 @@ describe('precompiler', function() {
     Precompiler.cli({templates: [emptyTemplate], simple: true});
     equal(log, 'simple\n');
   });
+  it('should default to simple templates', function() {
+    Handlebars.precompile = function() { return 'simple'; };
+    Precompiler.cli({templates: [{source: ''}]});
+    equal(log, 'simple\n');
+  });
   it('should output amd templates', function() {
     Handlebars.precompile = function() { return 'amd'; };
     Precompiler.cli({templates: [emptyTemplate], amd: true});
@@ -197,6 +207,42 @@ describe('precompiler', function() {
       });
     });
 
+    it('should accept string inputs', function(done) {
+      var opts = {string: ''};
+      Precompiler.loadTemplates(opts, function(err, opts) {
+        equal(opts.templates[0].name, undefined);
+        equal(opts.templates[0].source, '');
+        done(err);
+      });
+    });
+    it('should accept string array inputs', function(done) {
+      var opts = {string: ['', 'bar'], name: ['beep', 'boop']};
+      Precompiler.loadTemplates(opts, function(err, opts) {
+        equal(opts.templates[0].name, 'beep');
+        equal(opts.templates[0].source, '');
+        equal(opts.templates[1].name, 'boop');
+        equal(opts.templates[1].source, 'bar');
+        done(err);
+      });
+    });
+    it('should accept stdin input', function(done) {
+      var stdin = require('mock-stdin').stdin();
+      Precompiler.loadTemplates({string: '-'}, function(err, opts) {
+        equal(opts.templates[0].source, 'foo');
+        done(err);
+      });
+      stdin.send('fo');
+      stdin.send('o');
+      stdin.end();
+    });
+    it('error on name missing', function(done) {
+      var opts = {string: ['', 'bar']};
+      Precompiler.loadTemplates(opts, function(err) {
+        equal(err.message, 'Number of names did not match the number of string inputs');
+        done();
+      });
+    });
+
     it('should complete when no args are passed', function(done) {
       Precompiler.loadTemplates({}, function(err, opts) {
         equal(opts.templates.length, 0);
